# Playwright 错误索引 针对特定 Playwright 错误消息的快速参考手册。找到你的错误,了解原因,应用修复。 > **如何使用**:在此文件中搜索你在终端或测试报告中看到的确切错误文本。每条记录都给出了原因和可行的修复方案。 --- ## 定位器与元素错误 --- ### "locator.click: Target closed" **原因**:Playwright 完成对元素的操作之前,页面或框架发生了导航跳转(或已被关闭)。 **常见触发场景**: - 点击触发导航的链接时,另一个操作仍在等待中 - 表单提交导致页面重新加载,而后续的 `click()` 或 `fill()` 尚未完成 - 在 `finally` 块中调用了 `page.close()` 或 `context.close()`,与正在执行的操作产生竞态 - 应用程序中未处理的异常触发了错误页面的重定向 **修复**:在执行下一个操作之前等待导航完成,或者使用 `Promise.all` 协调点击和导航。 ```typescript // TypeScript — 等待由点击链接引起的导航完成 await Promise.all([ page.waitForURL("**/dashboard"), page.getByRole("link", { name: "Dashboard" }).click(), ]) ``` ```javascript // JavaScript — 相同的模式 await Promise.all([ page.waitForURL("**/dashboard"), page.getByRole("link", { name: "Dashboard" }).click(), ]) ``` 如果页面是有意关闭的(例如弹窗),请在操作之前捕获引用: ```typescript // TypeScript — 处理自动关闭的弹窗 const popupPromise = page.waitForEvent("popup") await page.getByRole("button", { name: "Open popup" }).click() const popup = await popupPromise await popup.waitForLoadState() // 在弹窗关闭前与其交互 await popup.getByRole("button", { name: "Confirm" }).click() ``` ```javascript // JavaScript — 处理自动关闭的弹窗 const popupPromise = page.waitForEvent("popup") await page.getByRole("button", { name: "Open popup" }).click() const popup = await popupPromise await popup.waitForLoadState() await popup.getByRole("button", { name: "Confirm" }).click() ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md)、[core/multi-context-and-popups.md](multi-context-and-popups.md) --- ### "waiting for locator('...') to be visible" **原因**:Playwright 的自动等待超时,因为元素从未出现在 DOM 中或一直处于隐藏状态(例如 `display: none`、`visibility: hidden`、零尺寸或位于 `aria-hidden` 之后)。 **常见触发场景**: - 元素是条件渲染的,但条件未满足(缺少数据、未认证状态) - 内容是异步加载的,定位器在 API 响应返回之前就被执行了 - 选择器错误——角色、名称或测试 ID 中有拼写错误 - 元素存在但在屏幕之外或位于折叠的容器内 - CSS 动画或过渡使元素保持 `opacity: 0`,而 Playwright 尚未判定为可见 **修复**:首先使用 Playwright Inspector 确认定位器与你期望的元素匹配。然后确保元素出现的先决条件已满足。 ```typescript // TypeScript — 调试:检查定位器解析出什么 console.log(await page.getByRole("button", { name: "Submit" }).count()) // 如果为 0,则元素不在 DOM 中——检查选择器或页面状态 // 正确做法:先等待数据加载完成 await page.waitForResponse((resp) => resp.url().includes("/api/data") && resp.status() === 200) await expect(page.getByRole("button", { name: "Submit" })).toBeVisible() ``` ```javascript // JavaScript — 相同的做法 console.log(await page.getByRole("button", { name: "Submit" }).count()) await page.waitForResponse((resp) => resp.url().includes("/api/data") && resp.status() === 200) await expect(page.getByRole("button", { name: "Submit" })).toBeVisible() ``` **相关**:[core/locators.md](locators.md)、[core/assertions-and-waiting.md](assertions-and-waiting.md) --- ### "locator.click: Error: strict mode violation" **原因**:定位器匹配了多个元素,Playwright 的严格模式(默认启用)拒绝在模棱两可的匹配上执行操作。 **常见触发场景**: - 页面上存在多个按钮时使用 `getByRole('button')` - "Save" 和 "Save as draft" 同时存在时使用 `getByText('Save')` - 需要 `exact: true` 时使用了部分文本匹配 - 同一个组件被渲染了多次(例如在列表中) **修复**:使定位器更加具体,确保解析到唯一一个元素。 ```typescript // TypeScript // 不好——匹配了多个按钮 await page.getByRole("button", { name: "Save" }).click() // 好——使用精确匹配 await page.getByRole("button", { name: "Save", exact: true }).click() // 好——限定到特定容器 await page.getByRole("dialog").getByRole("button", { name: "Save" }).click() // 好——在顺序有意义时使用 .first()、.last()、.nth() await page.getByRole("listitem").first().click() // 好——针对列表项链式使用 filter await page .getByRole("listitem") .filter({ hasText: "Project Alpha" }) .getByRole("button", { name: "Delete" }) .click() ``` ```javascript // JavaScript // 不好——匹配了多个按钮 await page.getByRole("button", { name: "Save" }).click() // 好——使用精确匹配 await page.getByRole("button", { name: "Save", exact: true }).click() // 好——限定到特定容器 await page.getByRole("dialog").getByRole("button", { name: "Save" }).click() // 好——在顺序有意义时使用 .first()、.last()、.nth() await page.getByRole("listitem").first().click() // 好——针对列表项链式使用 filter await page .getByRole("listitem") .filter({ hasText: "Project Alpha" }) .getByRole("button", { name: "Delete" }) .click() ``` **相关**:[core/locators.md](locators.md)、[core/locator-strategy.md](locator-strategy.md) --- ### "Error: expect(locator).toBeVisible() — locator resolved to X elements" **原因**:与上述严格模式违规的根因相同——断言的定位器匹配了多个元素,因此 Playwright 无法确定要对哪个元素进行断言。 **常见触发场景**: - 与上述严格模式违规的触发场景相同 - 当存在多个 `.item` 元素时写 `expect(page.locator('.item')).toBeVisible()` **修复**:将定位器缩小到单个元素(参见上方严格模式违规的修复方法)。或者,如果你有意要检查所有匹配: ```typescript // TypeScript — 断言数量而非可见性 await expect(page.getByRole("listitem")).toHaveCount(5) // 或者对特定元素进行断言 await expect(page.getByRole("listitem").first()).toBeVisible() // 或者使用循环断言所有元素都可见 for (const item of await page.getByRole("listitem").all()) { await expect(item).toBeVisible() } ``` ```javascript // JavaScript — 断言数量而非可见性 await expect(page.getByRole("listitem")).toHaveCount(5) // 或者对特定元素进行断言 await expect(page.getByRole("listitem").first()).toBeVisible() // 或者使用循环断言所有元素都可见 for (const item of await page.getByRole("listitem").all()) { await expect(item).toBeVisible() } ``` **相关**:[core/locators.md](locators.md)、[core/assertions-and-waiting.md](assertions-and-waiting.md) --- ### "locator.fill: Error: Element is not an , or [contenteditable] element" **原因**:`fill()` 只能用于 ``、`` 或带有 `contenteditable` 属性的元素。定位器解析到了不同的元素类型(例如 ``、`` 或 `` 本身)。 **常见触发场景**: - 按标签文本定位,但标签和输入框没有通过 `for`/`id` 正确关联 - 使用了 `getByText()` 而不是 `getByLabel()` 或 `getByRole('textbox')` - 自定义组件将实际的 `` 包裹在样式化的 `` 中 - 富文本编辑器在内部元素上使用了 `contenteditable`,而不是外层包装器 **修复**:定位到实际的输入元素。 ```typescript // TypeScript // 不好——定位到了标签,而不是输入框 await page.getByText("Email").fill("user@example.com") // 好——getByLabel 找到关联的输入框 await page.getByLabel("Email").fill("user@example.com") // 好——按角色定位 await page.getByRole("textbox", { name: "Email" }).fill("user@example.com") // 对于 contenteditable 富文本编辑器 await page.locator('[contenteditable="true"]').fill("Hello world") // 如果 fill() 仍然不起作用(例如自定义组件),使用键盘输入 await page.getByRole("textbox", { name: "Email" }).click() await page.keyboard.type("user@example.com") ``` ```javascript // JavaScript // 不好——定位到了标签,而不是输入框 await page.getByText("Email").fill("user@example.com") // 好——getByLabel 找到关联的输入框 await page.getByLabel("Email").fill("user@example.com") // 好——按角色定位 await page.getByRole("textbox", { name: "Email" }).fill("user@example.com") // 对于 contenteditable 富文本编辑器 await page.locator('[contenteditable="true"]').fill("Hello world") // 如果 fill() 仍然不起作用(例如自定义组件),使用键盘输入 await page.getByRole("textbox", { name: "Email" }).click() await page.keyboard.type("user@example.com") ``` **相关**:[core/locators.md](locators.md)、[core/forms-and-validation.md](forms-and-validation.md) --- ### "Error: elementHandle.click: Node is not an Element" **原因**:你正在使用旧的 ElementHandle API,并且该句柄已经过期——底层的 DOM 节点已被移除或被重新渲染所替换。 **常见触发场景**: - 将 `elementHandle` 存储在变量中,并在导航或 React/Vue 重新渲染后使用它 - 使用 `page.$()` 或 `page.$$()` 而不是 Locator API - 框架的水合(hydration)过程用客户端渲染的节点替换了服务端渲染的节点 **修复**:从 ElementHandle 切换到 Locator。Locator 在每次操作时都会重新查询 DOM,永不过期。 ```typescript // TypeScript // 不好——过期的句柄 const button = await page.$("button.submit") // ... 页面重新渲染 ... await button!.click() // Node is not an Element // 好——locator 始终重新查询 const button = page.getByRole("button", { name: "Submit" }) // ... 页面重新渲染 ... await button.click() // 正常运行 ``` ```javascript // JavaScript // 不好——过期的句柄 const button = await page.$("button.submit") // ... 页面重新渲染 ... await button.click() // Node is not an Element // 好——locator 始终重新查询 const button = page.getByRole("button", { name: "Submit" }) // ... 页面重新渲染 ... await button.click() // 正常运行 ``` **相关**:[core/locators.md](locators.md) --- ### "Error: locator.click: Timeout 30000ms exceeded" **原因**:元素已在 DOM 中找到,但在超时时间内未能变得可操作。"可操作"意味着可见、稳定(无动画)、已启用且未被其他元素遮挡。 **常见触发场景**: - 遮罩层、模态框或提示框覆盖了元素 - 元素处于禁用状态(`disabled` 属性或 `aria-disabled="true"`) - CSS 动画尚未完成(元素仍在移动) - 加载中的旋转图标覆盖在按钮上方 - 元素位于视口之外,Playwright 的自动滚动失败 **修复**:使用 trace 识别是什么阻止了操作,然后解决问题。 ```typescript // TypeScript // 第 1 步:启用 tracing 以进行诊断 // 在 playwright.config.ts 中: // use: { trace: 'on-first-retry' } // 然后运行:npx playwright show-trace trace.zip // 常见修复:等待遮罩层消失 await expect(page.locator(".loading-overlay")).toBeHidden() await page.getByRole("button", { name: "Submit" }).click() // 常见修复:当你确认遮罩层无害时使用强制点击 // (谨慎使用——这会跳过可操作性检查) await page.getByRole("button", { name: "Submit" }).click({ force: true }) // 常见修复:等待元素变为可用 await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled() await page.getByRole("button", { name: "Submit" }).click() ``` ```javascript // JavaScript // 常见修复:等待遮罩层消失 await expect(page.locator(".loading-overlay")).toBeHidden() await page.getByRole("button", { name: "Submit" }).click() // 常见修复:强制点击(谨慎使用) await page.getByRole("button", { name: "Submit" }).click({ force: true }) // 常见修复:等待元素变为可用 await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled() await page.getByRole("button", { name: "Submit" }).click() ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md)、[core/debugging.md](debugging.md) --- ## 导航与页面错误 --- ### "page.goto: net::ERR_CONNECTION_REFUSED" **原因**:Playwright 无法连接到目标 URL。服务器未运行,或未在预期的主机/端口上监听。 **常见触发场景**: - 运行测试前忘记启动开发服务器 - 开发服务器端口与配置中的 `baseURL` 不一致 - 服务器在 `127.0.0.1` 上监听,但测试使用的是 `localhost`(或反之),且操作系统解析方式不同 - 在 CI 中,应用构建/启动步骤静默失败 - Docker 容器网络:应用在容器内运行,但测试试图访问 `localhost` **修复**:使用 `webServer` 配置选项让 Playwright 自动启动和管理开发服务器。 ```typescript // TypeScript — playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ webServer: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, timeout: 120_000, // 服务器启动等待 2 分钟 }, use: { baseURL: "http://localhost:3000", }, }) ``` ```javascript // JavaScript — playwright.config.js const { defineConfig } = require("@playwright/test") module.exports = defineConfig({ webServer: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, timeout: 120_000, }, use: { baseURL: "http://localhost:3000", }, }) ``` **相关**:[core/configuration.md](configuration.md)、[ci/ci-github-actions.md](../ci/ci-github-actions.md) --- ### "page.goto: Timeout 30000ms exceeded" **原因**:页面未能在导航超时时间内达到 `load` 状态(默认情况下)。服务器有响应,但页面加载完整花费的时间过长。 **常见触发场景**: - 大型资源(图片、字体、脚本)拖慢了页面加载 - 第三方脚本(分析工具、广告)阻塞了加载事件 - 页面在变得可交互之前发出了大量 API 调用 - 服务器响应了一个耗时的重定向链 **修复**:使用更合适的 `waitUntil` 选项或增大导航超时时间。 ```typescript // TypeScript // 如果不需要所有资源,使用 'domcontentloaded' 代替 'load' await page.goto("/dashboard", { waitUntil: "domcontentloaded" }) // 或者为已知的慢页面增加导航超时时间 await page.goto("/dashboard", { timeout: 60_000 }) // 或者在配置中全局设置 // playwright.config.ts export default defineConfig({ use: { navigationTimeout: 60_000, }, }) ``` ```javascript // JavaScript await page.goto("/dashboard", { waitUntil: "domcontentloaded" }) await page.goto("/dashboard", { timeout: 60_000 }) // playwright.config.js module.exports = defineConfig({ use: { navigationTimeout: 60_000, }, }) ``` **相关**:[core/configuration.md](configuration.md) --- ### "Error: page.goto: Navigation failed because page was closed!" **原因**:`goto()` 还在进行中时,页面或浏览器上下文就被关闭了。这通常发生在拆卸/清理的竞态条件下。 **常见触发场景**: - `afterEach` 或 fixture 在导航完成前关闭了页面/上下文 - 另一个测试或钩子过早调用了 `browser.close()` - 测试失败,异步导航仍在等待时清理逻辑执行了 - 在异步操作开始后使用了 `test.fixme()` 或 `test.skip()` **修复**:确保所有页面操作完成后才执行清理。使用 fixtures 进行生命周期管理,而不是手动 `afterEach`。 ```typescript // TypeScript // 不好——测试体与清理之间存在竞态条件 test.afterEach(async ({ page }) => { await page.close() // 可能与正在进行的导航产生竞态 }) // 好——让 Playwright 通过 fixtures 管理页面生命周期 // Playwright 会自动为每个测试创建和销毁页面 // 无需手动清理 // 如果需要自定义清理,请确保先等待所有导航完成 test("navigates to dashboard", async ({ page }) => { const responsePromise = page.waitForResponse("**/api/user") await page.goto("/dashboard") await responsePromise // 确保所有异步操作完成 // 测试断言在此 }) ``` ```javascript // JavaScript — 相同的模式 test("navigates to dashboard", async ({ page }) => { const responsePromise = page.waitForResponse("**/api/user") await page.goto("/dashboard") await responsePromise // 测试断言在此 }) ``` **相关**:[core/fixtures-and-hooks.md](fixtures-and-hooks.md) --- ### "page.waitForNavigation: Timeout 30000ms exceeded" **原因**:`waitForNavigation()` 等待一个完整的页面导航,但该导航从未发生。在 SPA 中,客户端路由不会触发导航事件。 **常见触发场景**: - 在使用 React Router、Vue Router 或 Next.js 客户端导航的 SPA 中使用了 `waitForNavigation()` - 在 `waitForNavigation()` 被调用之前导航已经发生(竞态条件) - 使用了已废弃的模式,而 `waitForURL()` 才是正确的替代方法 **修复**:将 `waitForNavigation()` 替换为 `waitForURL()`,后者同时适用于 SPA 和传统的页面加载。 ```typescript // TypeScript // 不好——已废弃、易产生竞态、不支持 SPA 路由 await page.waitForNavigation() // 好——同时适用于 SPA 和完整导航 await page.getByRole("link", { name: "Dashboard" }).click() await page.waitForURL("**/dashboard") // 好——结合 glob 模式使用 await page.waitForURL(/\/dashboard\/\d+/) // 好——对于 SPA 路由变化,断言可见内容 await page.getByRole("link", { name: "Settings" }).click() await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible() ``` ```javascript // JavaScript // 不好——已废弃、易产生竞态 await page.waitForNavigation() // 好——同时适用于 SPA 和完整导航 await page.getByRole("link", { name: "Dashboard" }).click() await page.waitForURL("**/dashboard") // 好——结合 glob 模式使用 await page.waitForURL(/\/dashboard\/\d+/) // 好——对于 SPA 路由变化,断言可见内容 await page.getByRole("link", { name: "Settings" }).click() await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible() ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md) --- ### "Error: frame.goto: Frame was detached" **原因**:Playwright 正在导航或与该 iframe 交互时,它被从 DOM 中移除了(已分离)。 **常见触发场景**: - 父页面重新渲染并重新创建了 iframe 元素 - 父页面上的 JavaScript 移除后又重新添加了 iframe - SPA 路由变化卸载了包含 iframe 的组件 - 广告 iframe 自行重新加载 **修复**:在父页面更新后重新获取 frame 引用。 ```typescript // TypeScript // 不好——frame 引用变得过期 const frame = page.frameLocator("#my-iframe") // ... 父页面重新渲染 ... await frame.getByRole("button").click() // Frame was detached // 好——使用 frameLocator(每次重新求值) // frameLocator 本身是惰性的,但它指向的 frame 必须存在 await expect(page.frameLocator("#my-iframe").getByRole("button")).toBeVisible() await page.frameLocator("#my-iframe").getByRole("button").click() // 好——等待 iframe 在重新渲染后出现 await expect(page.locator("#my-iframe")).toBeAttached() await page.frameLocator("#my-iframe").getByRole("button").click() ``` ```javascript // JavaScript // 好——frameLocator 惰性重新求值 await expect(page.frameLocator("#my-iframe").getByRole("button")).toBeVisible() await page.frameLocator("#my-iframe").getByRole("button").click() // 好——等待 iframe 在重新渲染后出现 await expect(page.locator("#my-iframe")).toBeAttached() await page.frameLocator("#my-iframe").getByRole("button").click() ``` **相关**:[core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) --- ## 测试框架错误 --- ### "Error: Test timeout of 30000ms exceeded" **原因**:整个测试(包括所有钩子和断言)未能在配置的测试超时时间内完成。 **常见触发场景**: - 等待一个永不出现的元素(错误的定位器、缺少数据) - 未解决的 `page.waitForEvent()`——预期的事件从未触发 - 缓慢的测试环境(资源受限的 CI) - 单个测试中包含过多顺序操作 - `beforeEach` 钩子执行了昂贵的设置(登录、数据填充) **修复**:使用 `test.step()` 和 traces 识别慢速步骤,然后修复根本原因或调整超时时间。 ```typescript // TypeScript // 为特定的慢速测试增加超时时间 test("generates large report", async ({ page }) => { test.setTimeout(120_000) // 仅此测试 2 分钟 // ... }) // 在配置中全局增加超时时间 // playwright.config.ts export default defineConfig({ timeout: 60_000, // 每个测试 60 秒 }) // 更好的做法:使用 test.step() 诊断哪些步骤慢 test("checkout flow", async ({ page }) => { await test.step("add item to cart", async () => { await page.goto("/products") await page.getByRole("button", { name: "Add to Cart" }).click() }) await test.step("complete checkout", async () => { await page.getByRole("link", { name: "Cart" }).click() await page.getByRole("button", { name: "Checkout" }).click() }) }) ``` ```javascript // JavaScript test("generates large report", async ({ page }) => { test.setTimeout(120_000) // ... }) // playwright.config.js module.exports = defineConfig({ timeout: 60_000, }) test("checkout flow", async ({ page }) => { await test.step("add item to cart", async () => { await page.goto("/products") await page.getByRole("button", { name: "Add to Cart" }).click() }) await test.step("complete checkout", async () => { await page.getByRole("link", { name: "Cart" }).click() await page.getByRole("button", { name: "Checkout" }).click() }) }) ``` **相关**:[core/configuration.md](configuration.md)、[core/debugging.md](debugging.md) --- ### "Error: expect(received).toMatchSnapshot()" **原因**:当前的截图或文本与存储的基线快照不匹配。 **常见触发场景**: - 合法的 UI 更改需要更新快照 - 本地与 CI 之间的渲染差异(字体、抗锯齿、操作系统级渲染) - 动态内容(时间戳、随机 ID、广告)在运行之间发生变化 - 不同环境之间的视口大小或设备模拟设置不同 **修复**:如果更改是有意的,则更新快照;或者遮罩/隐藏动态区域。 ```typescript // TypeScript // 在有意更改后更新快照 // 运行:npx playwright test --update-snapshots // 在截图前遮罩动态内容 await expect(page).toHaveScreenshot("dashboard.png", { mask: [page.locator(".timestamp"), page.locator(".random-ad")], maxDiffPixelRatio: 0.01, // 允许 1% 的像素差异 }) // 使用带清理器的文本快照处理动态值 const content = await page.getByRole("main").textContent() const sanitized = content!.replace(/\d{4}-\d{2}-\d{2}/g, "DATE") expect(sanitized).toMatchSnapshot("dashboard-content.txt") ``` ```javascript // JavaScript // 更新快照:npx playwright test --update-snapshots await expect(page).toHaveScreenshot("dashboard.png", { mask: [page.locator(".timestamp"), page.locator(".random-ad")], maxDiffPixelRatio: 0.01, }) const content = await page.getByRole("main").textContent() const sanitized = content.replace(/\d{4}-\d{2}-\d{2}/g, "DATE") expect(sanitized).toMatchSnapshot("dashboard-content.txt") ``` **相关**:[core/visual-regression.md](visual-regression.md) --- ### "Error: browserType.launch: Executable doesn't exist" **原因**:Playwright 浏览器二进制文件未在此机器上安装,或者安装的版本与当前的 Playwright 版本不匹配。 **常见触发场景**: - 新克隆项目后未运行安装步骤 - 升级了 `@playwright/test` 但没有重新安装浏览器 - CI 环境缺少安装步骤 - 使用 `npm ci` 会清理 `node_modules`,但不会触发浏览器安装的 postinstall 脚本 - 安装了多个 Playwright 版本(全局与本地冲突) **修复**:安装浏览器。 ```bash # 安装所有浏览器 npx playwright install # 只安装需要的浏览器(CI 中更快) npx playwright install chromium # 安装浏览器及操作系统依赖(CI/Docker 中需要) npx playwright install --with-deps # 如果在 CI Dockerfile 中,将此添加到 Dockerfile # RUN npx playwright install --with-deps chromium ``` 对于 CI 流水线,始终包含安装步骤: ```yaml # GitHub Actions 示例 - name: Install Playwright Browsers run: npx playwright install --with-deps ``` **相关**:[ci/ci-github-actions.md](../ci/ci-github-actions.md)、[ci/docker-and-containers.md](../ci/docker-and-containers.md) --- ### "Error: Cannot use import statement outside a module" **原因**:Node.js 以 CommonJS 模式运行测试文件,但文件使用了 ES 模块的 `import` 语法,且没有正确的配置。 **常见触发场景**: - 缺少或配置错误的 `tsconfig.json` - 使用了 `.ts` 文件,但 `@playwright/test` 未能正确解析 TypeScript - `package.json` 没有 `"type": "module"` 设置,但测试文件使用了 `import` - 直接使用 `node` 而不是通过 `npx playwright test` 运行测试文件 - 冲突的 Babel 或 ts-node 配置 **修复**:确保 TypeScript 配置正确,并始终通过 Playwright CLI 运行测试。 ```bash # 始终通过 Playwright CLI 运行测试——绝不要直接使用 node npx playwright test # 不要这样: # node tests/example.spec.ts <-- 会失败 ``` ```typescript // TypeScript — tsconfig.json(最小工作配置) { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true } } ``` 如果使用 JavaScript 而非 TypeScript: ```javascript // JavaScript — 确保 package.json 有 "type": "module" // 或将文件重命名为 .mjs // 在 playwright.config.js 中——如果 package.json 缺少 "type": "module",则使用 require const { defineConfig } = require("@playwright/test") module.exports = defineConfig({ /* ... */ }) ``` **相关**:[core/configuration.md](configuration.md) --- ### "Error: fixture \"xxx\" has already been registered" **原因**:在多个 `test.extend()` 调用中定义了同名的自定义 fixture,且这些调用被合并了;或者同一 fixture 名称在同一个扩展中被注册了两次。 **常见触发场景**: - 两个不同的 fixture 文件都定义了同名 fixture - 从多个文件中导入扩展后的 `test` 对象,这些文件各自添加了重叠的 fixture - 复制粘贴 fixture 定义时没有重命名 **修复**:确保每个 fixture 名称在你合并后的 fixture 链中是唯一的。使用一个统一的 fixture 文件来组合所有扩展。 ```typescript // TypeScript // 不好——两个独立的扩展包含同名的 fixture // fixtures/auth.ts export const test = base.extend<{ user: User }>({ user: async ({}, use) => { /* ... */ }, }) // fixtures/data.ts — 冲突:"user" 已存在 export const test = base.extend<{ user: User }>({ user: async ({}, use) => { /* ... */ }, }) // 好——单个合并后的 fixture 文件 // fixtures/index.ts import { test as base } from "@playwright/test" type MyFixtures = { authenticatedUser: User testData: TestData } export const test = base.extend({ authenticatedUser: async ({}, use) => { const user = await createUser() await use(user) await deleteUser(user) }, testData: async ({}, use) => { const data = await seedData() await use(data) await cleanupData(data) }, }) export { expect } from "@playwright/test" ``` ```javascript // JavaScript // 好——单个合并后的 fixture 文件 // fixtures/index.js const { test: base } = require("@playwright/test") const test = base.extend({ authenticatedUser: async ({}, use) => { const user = await createUser() await use(user) await deleteUser(user) }, testData: async ({}, use) => { const data = await seedData() await use(data) await cleanupData(data) }, }) module.exports = { test } ``` **相关**:[core/fixtures-and-hooks.md](fixtures-and-hooks.md) --- ## 网络与 API 错误 --- ### "page.route: Pattern should start with..." **原因**:传递给 `page.route()` 的 URL 模式不符合预期格式。Playwright 期望一个 glob 模式(以 `**/` 或 `http` 开头)、一个正则表达式或一个谓词函数。 **常见触发场景**: - 传递裸路径如 `/api/users` 而不是 `**/api/users` - 对于相对模式缺少 `**` glob 前缀 - 传递了对象或无效类型而不是字符串/正则表达式 **修复**:使用正确的模式格式。 ```typescript // TypeScript // 不好——没有 glob 前缀的裸路径 await page.route("/api/users", (route) => route.fulfill({ body: "[]" })) // 好——glob 模式 await page.route("**/api/users", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Alice" }]), }) ) // 好——正则表达式模式 await page.route(/\/api\/users\/\d+/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: 1, name: "Alice" }), }) ) // 好——用于复杂匹配的谓词函数 await page.route( (url) => url.pathname.startsWith("/api/") && url.searchParams.has("filter"), (route) => route.fulfill({ body: "[]" }) ) ``` ```javascript // JavaScript // 好——glob 模式 await page.route("**/api/users", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Alice" }]), }) ) // 好——正则表达式模式 await page.route(/\/api\/users\/\d+/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: 1, name: "Alice" }), }) ) // 好——谓词函数 await page.route( (url) => url.pathname.startsWith("/api/") && url.searchParams.has("filter"), (route) => route.fulfill({ body: "[]" }) ) ``` **相关**:[core/network-mocking.md](network-mocking.md) --- ### "Error: apiRequestContext.get: connect ECONNREFUSED" **原因**:通过 `request.get()`(或 `.post()`、`.put()` 等)发出的 API 请求无法连接到目标服务器。与 `page.goto: net::ERR_CONNECTION_REFUSED` 的根本原因相同,但针对 API 测试。 **常见触发场景**: - 使用 `request` fixture 时 API 服务器未运行 - 配置或 API 请求上下文中的 `baseURL` 错误 - 对尚未启动的服务运行 API 测试 - 在 CI 中,API 服务容器尚未就绪 **修复**:确保 API 服务器正在运行。使用 `webServer` 配置自动启动它,或添加健康检查。 ```typescript // TypeScript — playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ webServer: [ { command: "npm run start:api", url: "http://localhost:4000/health", reuseExistingServer: !process.env.CI, }, { command: "npm run start:web", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, ], use: { baseURL: "http://localhost:3000", }, }) // 在测试中,使用带有显式 baseURL 的 request fixture 进行 API 调用 test("fetch users", async ({ request }) => { const response = await request.get("http://localhost:4000/api/users") expect(response.ok()).toBeTruthy() }) ``` ```javascript // JavaScript — playwright.config.js const { defineConfig } = require("@playwright/test") module.exports = defineConfig({ webServer: [ { command: "npm run start:api", url: "http://localhost:4000/health", reuseExistingServer: !process.env.CI, }, { command: "npm run start:web", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, ], use: { baseURL: "http://localhost:3000", }, }) ``` **相关**:[core/api-testing.md](api-testing.md)、[core/configuration.md](configuration.md) --- ### "Request was not handled: GET https://..." **原因**:使用 `page.route()` 注册了路由处理器并设置了 `{ times: N }`,或者调用了 `page.unrouteAll()`,之后一个匹配该模式的后续请求未被拦截——Playwright 警告你有一个请求未被处理。 **常见触发场景**: - 使用 `page.route()` 时设置了 `{ times: 1 }`,但应用发出了多次相同请求 - 调用了 `route.fallback()` 或 `route.continue()`,但没有处理器来捕获该请求 - 在测试中途调用了 `page.unrouteAll({ behavior: 'wait' })`,而应用又发出了另一个请求 **修复**:确保你的路由处理器覆盖所有预期的请求,或者让未处理的请求正常通过。 ```typescript // TypeScript // 不好——只处理第一次请求,第二次请求未处理 await page.route("**/api/data", (route) => route.fulfill({ body: "{}" }), { times: 1 }) // 好——处理所有到此 URL 的请求 await page.route("**/api/data", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [] }), }) ) // 好——如果只想拦截一次,后续请求使用 waitForResponse await page.route("**/api/data", (route) => route.fulfill({ body: '{"items": []}' }), { times: 1 }) // 对于第二次调用,让它去真实服务器(不需要路由) ``` ```javascript // JavaScript // 好——处理所有到此 URL 的请求 await page.route("**/api/data", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [] }), }) ) ``` **相关**:[core/network-mocking.md](network-mocking.md) --- ## 认证与状态错误 --- ### "Error: browserContext.storageState: No such file" **原因**:配置或测试中引用的 `storageState` 路径指向一个不存在的文件。认证状态文件尚未生成。 **常见触发场景**: - 在运行认证设置项目之前就运行了测试 - `globalSetup` 或认证设置项目未成功完成 - 配置中的文件路径与设置中使用的路径不匹配 - `.gitignore` 排除了 storage state 文件,新克隆后未重新生成 - CI 缓存过期,状态文件未被重新创建 **修复**:确保认证设置先运行,并且文件路径保持一致。 ```typescript // TypeScript — playwright.config.ts import { defineConfig } from "@playwright/test" const authFile = "playwright/.auth/user.json" export default defineConfig({ projects: [ // 设置项目先运行,创建认证状态文件 { name: "setup", testMatch: /.*\.setup\.ts/, }, { name: "chromium", use: { storageState: authFile, }, dependencies: ["setup"], // 确保 setup 先运行 }, ], }) // auth.setup.ts import { test as setup, expect } from "@playwright/test" const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("password123") await page.getByRole("button", { name: "Sign in" }).click() await page.waitForURL("/dashboard") // 保存认证后的状态 await page.context().storageState({ path: authFile }) }) ``` ```javascript // JavaScript — playwright.config.js const { defineConfig } = require("@playwright/test") const authFile = "playwright/.auth/user.json" module.exports = defineConfig({ projects: [ { name: "setup", testMatch: /.*\.setup\.js/, }, { name: "chromium", use: { storageState: authFile, }, dependencies: ["setup"], }, ], }) // auth.setup.js const { test: setup, expect } = require("@playwright/test") const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("password123") await page.getByRole("button", { name: "Sign in" }).click() await page.waitForURL("/dashboard") await page.context().storageState({ path: authFile }) }) ``` 确保认证目录存在并已添加到 `.gitignore`: ```bash mkdir -p playwright/.auth echo "playwright/.auth/" >> .gitignore ``` **相关**:[core/authentication.md](authentication.md)、[core/auth-flows.md](auth-flows.md) --- ### "Error: Target page, context or browser has been closed" **原因**:代码尝试使用一个已经关闭或销毁的 `page`、`context` 或 `browser` 对象。 **常见触发场景**: - 在 `page.close()` 被调用后使用了 `page` 引用 - 将 `page` 或 `context` 存储在模块级变量中并在多个测试间重用 - 在异步回调仍在运行时,fixture 拆除了浏览器上下文 - `beforeAll` 创建了一个上下文,但该上下文在所有测试完成使用之前就被关闭了 - 手动使用 `browser.newContext()` 但忘记管理其生命周期 **修复**:绝不要在共享的可变状态中存储 page/context 引用。使用 Playwright fixtures 进行生命周期管理。 ```typescript // TypeScript // 不好——共享的模块级变量 let sharedPage: Page test.beforeAll(async ({ browser }) => { sharedPage = await browser.newPage() }) test.afterAll(async () => { await sharedPage.close() }) test("first", async () => { await sharedPage.goto("/") // 如果另一个 describe 的 afterAll 先运行,可能会失败 }) // 好——使用内置的 page fixture(每个测试一个,自动管理) test("first", async ({ page }) => { await page.goto("/") // 始终是一个全新的、打开的页面 }) // 好——对于跨测试的共享状态,使用 worker 作用域的 fixture import { test as base } from "@playwright/test" export const test = base.extend<{}, { adminContext: BrowserContext }>({ adminContext: [ async ({ browser }, use) => { const context = await browser.newContext() await use(context) await context.close() }, { scope: "worker" }, ], }) ``` ```javascript // JavaScript // 好——使用内置的 page fixture test("first", async ({ page }) => { await page.goto("/") }) // 好——对于共享状态,使用 worker 作用域的 fixture const { test: base } = require("@playwright/test") const test = base.extend({ adminContext: [ async ({ browser }, use) => { const context = await browser.newContext() await use(context) await context.close() }, { scope: "worker" }, ], }) ``` **相关**:[core/fixtures-and-hooks.md](fixtures-and-hooks.md) --- ## CI 特定错误 --- ### "Error: browserType.launch: Browser closed unexpectedly" **原因**:浏览器进程在启动后立即崩溃,通常是因为 CI 环境中缺少所需的操作系统级依赖。 **常见触发场景**: - 在最小化 Docker 镜像(如 `node:alpine`)上运行,缺少浏览器依赖 - Linux CI 上缺少共享库(`libgbm`、`libnss3`、`libatk-bridge` 等) - 共享内存不足(`/dev/shm` 太小)——浏览器使用共享内存进行渲染 - 在 Docker 中以 root 身份运行,没有 `--no-sandbox` 参数(Chromium 拒绝以 root 身份运行沙箱) - CI 中因内存不足被杀死(浏览器进程被操作系统 OOM 杀死) **修复**:安装操作系统级依赖并配置环境。 ```bash # 安装浏览器及其操作系统依赖(推荐) npx playwright install --with-deps chromium # 如果使用 Docker,使用官方的 Playwright 镜像 # Dockerfile FROM mcr.microsoft.com/playwright:v1.50.0-noble WORKDIR /app COPY . . RUN npm ci RUN npx playwright install chromium CMD ["npx", "playwright", "test"] ``` 针对共享内存问题: ```yaml # GitHub Actions — 增加 /dev/shm jobs: test: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v1.50.0-noble options: --shm-size=2gb # Docker run — 增加共享内存 # docker run --shm-size=2gb my-test-image ``` 针对在 Docker 中以 root 身份运行: ```typescript // TypeScript — playwright.config.ts(仅用于 Docker/CI) export default defineConfig({ use: { launchOptions: { args: process.env.CI ? ["--no-sandbox", "--disable-setuid-sandbox"] : [], }, }, }) ``` ```javascript // JavaScript — playwright.config.js module.exports = defineConfig({ use: { launchOptions: { args: process.env.CI ? ["--no-sandbox", "--disable-setuid-sandbox"] : [], }, }, }) ``` **相关**:[ci/ci-github-actions.md](../ci/ci-github-actions.md)、[ci/docker-and-containers.md](../ci/docker-and-containers.md) --- ### "Error: Playwright Test needs to be invoked via 'npx playwright test'" **原因**:测试文件是直接通过 `node` 或 `ts-node` 执行的,而不是通过 Playwright 测试运行器。 **常见触发场景**: - 运行 `node tests/example.spec.ts` 或 `ts-node tests/example.spec.ts` - 配置错误的 IDE 运行配置直接调用了 `node` - 使用 Jest 或 Mocha 来运行从 `@playwright/test` 导入的 Playwright 测试文件 - CI 脚本调用了错误的命令 **修复**:始终使用 Playwright CLI 来运行测试。 ```bash # 正确的调用方式 npx playwright test # 运行所有测试 npx playwright test tests/login.spec.ts # 运行特定文件 npx playwright test --grep "login" # 运行匹配模式的测试 npx playwright test --project=chromium # 运行特定项目 npx playwright test --debug # 使用检查器运行 # 如果使用 package.json 脚本 # package.json # "scripts": { "test:e2e": "playwright test" } npm run test:e2e ``` 针对 IDE 配置(VS Code): ```json // .vscode/settings.json — 安装 Playwright VS Code 扩展而非自定义配置 { "playwright.reuseBrowser": true } ``` **相关**:[core/configuration.md](configuration.md) --- ## 其他常见错误 --- ### "Error: page.evaluate: Execution context was destroyed" **原因**:在 `page.evaluate()` 正在浏览器上下文中执行 JavaScript 时,页面发生了导航或重新加载。 **常见触发场景**: - 在页面处于导航过程中时调用 `evaluate()` - 页面上的定时器或事件在评估期间触发了重定向 - 在 `beforeEach` 中运行 `evaluate()`,与 `page.goto()` 产生竞态 **修复**:确保页面在评估前处于稳定状态。 ```typescript // TypeScript // 不好——evaluate 与导航产生竞态 await page.goto("/dashboard") const title = await page.evaluate(() => document.title) // 上下文可能已被销毁 // 好——先等待加载状态 await page.goto("/dashboard") await page.waitForLoadState("domcontentloaded") const title = await page.evaluate(() => document.title) // 好——优先使用基于定位器的断言(不需要 evaluate) await page.goto("/dashboard") await expect(page).toHaveTitle("Dashboard") ``` ```javascript // JavaScript // 好——先等待加载状态 await page.goto("/dashboard") await page.waitForLoadState("domcontentloaded") const title = await page.evaluate(() => document.title) // 好——优先使用基于定位器的断言 await page.goto("/dashboard") await expect(page).toHaveTitle("Dashboard") ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md) --- ### "Error: page.screenshot: Cannot take a screenshot larger than..." **原因**:请求的截图尺寸超过了 Playwright 的限制(通常为 16384x16384 像素)。在对非常长的页面进行全页截图时会发生这种情况。 **常见触发场景**: - 在无限滚动的页面上调用 `page.screenshot({ fullPage: true })` - 内容极高的页面(长数据表格、聊天记录) - CSS 错误导致元素高度巨大 **修复**:将截图裁剪到特定区域或限制视口大小。 ```typescript // TypeScript // 不好——在很高的页面上可能超出尺寸限制 await page.screenshot({ fullPage: true, path: "full.png" }) // 好——裁剪到特定区域 await page.screenshot({ path: "clipped.png", clip: { x: 0, y: 0, width: 1280, height: 2000 }, }) // 好——改为截取特定元素 await page.getByRole("main").screenshot({ path: "main-content.png" }) // 好——在全页截图前设置合理的视口大小 await page.setViewportSize({ width: 1280, height: 720 }) await page.screenshot({ fullPage: true, path: "full.png" }) ``` ```javascript // JavaScript // 好——裁剪到特定区域 await page.screenshot({ path: "clipped.png", clip: { x: 0, y: 0, width: 1280, height: 2000 }, }) // 好——截取特定元素 await page.getByRole("main").screenshot({ path: "main-content.png" }) ``` **相关**:[core/visual-regression.md](visual-regression.md) --- ### "Error: waiting for locator('...').toBeAttached()" **原因**:元素在断言超时时间内从未进入 DOM。与 `toBeVisible()` 不同,`toBeAttached()` 只检查 DOM 存在性——但该元素根本从未被渲染过。 **常见触发场景**: - 组件是条件渲染的,但条件未满足 - 提供数据的网络请求尚未完成 - 错误的选择器,不与任何元素匹配 - 元素由延迟加载的代码块渲染,但该代码块尚未下载完成 **修复**:验证元素渲染的先决条件。 ```typescript // TypeScript // 在断言之前确保数据已加载 await page.goto("/users") await page.waitForResponse((resp) => resp.url().includes("/api/users")) await expect(page.getByRole("table")).toBeAttached() // 对于延迟加载的内容,等待更长时间或触发加载 await page.getByRole("tab", { name: "Advanced" }).click() await expect(page.getByTestId("advanced-panel")).toBeAttached({ timeout: 10_000 }) ``` ```javascript // JavaScript await page.goto("/users") await page.waitForResponse((resp) => resp.url().includes("/api/users")) await expect(page.getByRole("table")).toBeAttached() await page.getByRole("tab", { name: "Advanced" }).click() await expect(page.getByTestId("advanced-panel")).toBeAttached({ timeout: 10_000 }) ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md)、[core/locators.md](locators.md) --- ### "Error: protocol error: Target.createTarget: Failed to create target" **原因**:浏览器无法创建新的标签页或上下文,通常是由于资源耗尽。 **常见触发场景**: - 打开了太多页面或上下文而没有关闭它们 - 测试中的内存泄漏——每个测试都打开一个上下文但从未关闭 - 在资源受限的 CI 机器上运行大量并行 worker - 浏览器进程在上一次崩溃后处于不稳定状态 **修复**:降低并行度、关闭未使用的上下文,或使用更少的 worker。 ```typescript // TypeScript — playwright.config.ts export default defineConfig({ // 如果资源有限,降低并行度 workers: process.env.CI ? 2 : undefined, // 限制每个 worker 一个浏览器上下文 fullyParallel: false, // 在 worker 内串行运行测试文件 }) ``` ```javascript // JavaScript — playwright.config.js module.exports = defineConfig({ workers: process.env.CI ? 2 : undefined, fullyParallel: false, }) ``` 如果你手动创建上下文,务必关闭它们: ```typescript // TypeScript test("multi-user test", async ({ browser }) => { const userContext = await browser.newContext() const adminContext = await browser.newContext() try { const userPage = await userContext.newPage() const adminPage = await adminContext.newPage() // ... 测试逻辑 } finally { await userContext.close() await adminContext.close() } }) ``` ```javascript // JavaScript test("multi-user test", async ({ browser }) => { const userContext = await browser.newContext() const adminContext = await browser.newContext() try { const userPage = await userContext.newPage() const adminPage = await adminContext.newPage() // ... 测试逻辑 } finally { await userContext.close() await adminContext.close() } }) ``` **相关**:[ci/parallel-and-sharding.md](../ci/parallel-and-sharding.md)、[core/multi-user-and-collaboration.md](multi-user-and-collaboration.md) --- ### "Error: expect(locator).toHaveText() — expected string but received array" **原因**:定位器匹配了多个元素,`toHaveText()` 返回了一个字符串数组而不是单个字符串。 **常见触发场景**: - 使用了宽泛的定位器如 `page.locator('.item')`,匹配了一个列表 - 期望一个单独的标题,但选择器匹配了多个 - 没有将定位器限定到特定容器 **修复**:要么将定位器缩小到一个元素,要么使用 `toHaveText()` 的数组形式。 ```typescript // TypeScript // 不好——定位器匹配了多个元素 await expect(page.locator(".item")).toHaveText("First Item") // 好——缩小到一个元素 await expect(page.locator(".item").first()).toHaveText("First Item") // 好——在期望多个元素时使用数组断言 await expect(page.locator(".item")).toHaveText(["First Item", "Second Item", "Third Item"]) // 好——使用更具体的定位器 await expect(page.getByRole("heading", { level: 1 })).toHaveText("Welcome") ``` ```javascript // JavaScript // 好——缩小到一个元素 await expect(page.locator(".item").first()).toHaveText("First Item") // 好——使用数组断言 await expect(page.locator(".item")).toHaveText(["First Item", "Second Item", "Third Item"]) // 好——使用更具体的定位器 await expect(page.getByRole("heading", { level: 1 })).toHaveText("Welcome") ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md)、[core/locators.md](locators.md) --- ### "Error: page.waitForSelector: Timeout 30000ms exceeded (deprecated)" **原因**:`page.waitForSelector()` 超时。此方法已废弃,推荐使用基于定位器的断言。 **常见触发场景**: - 从旧教程或从 Puppeteer 迁移时使用了旧的 `page.waitForSelector()` - 选择器不匹配 DOM 中的任何元素 - 元素存在但不符合状态要求(例如 `{ state: 'visible' }` 但元素处于隐藏状态) **修复**:将 `waitForSelector()` 替换为基于定位器的断言。 ```typescript // TypeScript // 不好——已废弃、无自动重试、不可组合 await page.waitForSelector(".loading", { state: "hidden" }) await page.waitForSelector(".content", { state: "visible" }) // 好——基于定位器的断言,带自动重试 await expect(page.locator(".loading")).toBeHidden() await expect(page.locator(".content")).toBeVisible() // 好——使用基于角色的定位器以获得更强的鲁棒性 await expect(page.getByRole("progressbar")).toBeHidden() await expect(page.getByRole("main")).toBeVisible() ``` ```javascript // JavaScript // 不好——已废弃 await page.waitForSelector(".loading", { state: "hidden" }) await page.waitForSelector(".content", { state: "visible" }) // 好——基于定位器的断言 await expect(page.locator(".loading")).toBeHidden() await expect(page.locator(".content")).toBeVisible() // 好——基于角色的定位器 await expect(page.getByRole("progressbar")).toBeHidden() await expect(page.getByRole("main")).toBeVisible() ``` **相关**:[core/locators.md](locators.md)、[core/assertions-and-waiting.md](assertions-and-waiting.md)、[migration/from-selenium.md](../migration/from-selenium.md) --- ### "Error: Test was expected to have a title matching /.../" **原因**:`expect(page).toHaveTitle()` 断言失败,因为页面标题在超时时间内未匹配期望的值或模式。 **常见触发场景**: - 页面尚未完成加载,标题仍是初始的空值或默认值 - SPA 在渲染后异步更新文档标题 - 期望的标题有拼写错误或大小写不匹配 - 页面重定向到了具有不同标题的错误页面 **修复**:确保页面已完全加载,并使用正确的期望值。 ```typescript // TypeScript // toHaveTitle 会自动重试,但要确保页面已加载 await page.goto("/dashboard") await expect(page).toHaveTitle("Dashboard | MyApp") // 使用正则表达式进行灵活匹配 await expect(page).toHaveTitle(/Dashboard/) // 调试:查看实际标题是什么 console.log(await page.title()) ``` ```javascript // JavaScript await page.goto("/dashboard") await expect(page).toHaveTitle("Dashboard | MyApp") // 使用正则表达式进行灵活匹配 await expect(page).toHaveTitle(/Dashboard/) // 调试:查看实际标题是什么 console.log(await page.title()) ``` **相关**:[core/assertions-and-waiting.md](assertions-and-waiting.md) --- ### "Error: page.type: Element is not focusable" **原因**:`page.type()` 或 `locator.type()` 在输入前无法聚焦目标元素。该元素可能处于隐藏、禁用状态,或者不是一个输入元素。 **常见触发场景**: - 元素被遮罩层或模态框覆盖 - 元素设置了 `tabindex="-1"`,本身不可聚焦 - 使用了 `type()` 而不是 `fill()`——在几乎所有情况下都应优先使用 `fill()` - 元素位于 Shadow DOM 内部,定位器无法穿透 **修复**:使用 `fill()` 代替 `type()`。仅在确实需要模拟逐个按键时才使用 `type()`。 ```typescript // TypeScript // 不好——type() 更慢且更脆弱 await page.locator("#search").type("hello") // 好——fill() 直接清除并设置值 await page.getByRole("searchbox").fill("hello") // 当真正需要逐键输入时(例如触发自动补全) await page.getByRole("searchbox").pressSequentially("hello", { delay: 100 }) ``` ```javascript // JavaScript // 好——fill() 是首选 await page.getByRole("searchbox").fill("hello") // 需要时逐键输入(例如自动补全) await page.getByRole("searchbox").pressSequentially("hello", { delay: 100 }) ``` **相关**:[core/locators.md](locators.md)、[core/forms-and-validation.md](forms-and-validation.md) --- ### "Error: page.setInputFiles: Non-multiple file input can only accept single file" **原因**:尝试向没有 `multiple` 属性的 `` 上传多个文件。 **常见触发场景**: - 向单文件上传输入框传递了一个文件数组 - 应用使用了一个自定义的上传组件,每次只能处理一个文件 **修复**:一次只上传一个文件,或者确保输入框支持多个文件。 ```typescript // TypeScript // 不好——输入框没有 multiple 属性 await page.getByLabel("Upload file").setInputFiles([ "file1.pdf", "file2.pdf", // 错误:非多文件输入 ]) // 好——单个文件 await page.getByLabel("Upload file").setInputFiles("file1.pdf") // 好——对于多文件输入(HTML 中必须有 multiple 属性) await page.getByLabel("Upload files").setInputFiles(["file1.pdf", "file2.pdf"]) // 清除文件输入 await page.getByLabel("Upload file").setInputFiles([]) ``` ```javascript // JavaScript // 好——单个文件 await page.getByLabel("Upload file").setInputFiles("file1.pdf") // 好——多个文件(输入框必须有 multiple 属性) await page.getByLabel("Upload files").setInputFiles(["file1.pdf", "file2.pdf"]) // 清除文件输入 await page.getByLabel("Upload file").setInputFiles([]) ``` **相关**:[core/file-operations.md](file-operations.md)、[core/file-upload-download.md](file-upload-download.md) --- ### "Error: browser.newContext: Could not parse content-type application/json" **原因**:`storageState` 选项指向了一个包含无效 JSON 内容的文件,或者该文件不是有效的 Playwright storage state 格式。 **常见触发场景**: - 认证设置写入了一个空文件或不完整的 JSON - storage state 文件被损坏或截断 - 手动编辑 storage state 文件引入了语法错误 - 认证设置静默失败(登录未完成)并保存了无效状态 **修复**:删除 storage state 文件并重新生成。 ```bash # 删除损坏的文件 rm -f playwright/.auth/user.json # 重新运行设置 npx playwright test --project=setup ``` 为防止此问题,在认证设置中添加错误检查: ```typescript // TypeScript — auth.setup.ts import { test as setup, expect } from "@playwright/test" const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("password123") await page.getByRole("button", { name: "Sign in" }).click() // 在保存状态前验证登录确实成功 await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await page.context().storageState({ path: authFile }) }) ``` ```javascript // JavaScript — auth.setup.js const { test: setup, expect } = require("@playwright/test") const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill("password123") await page.getByRole("button", { name: "Sign in" }).click() // 在保存状态前验证登录确实成功 await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await page.context().storageState({ path: authFile }) }) ``` **相关**:[core/authentication.md](authentication.md)、[core/auth-flows.md](auth-flows.md) --- ## 快速诊断清单 当你遇到上面未列出的错误时,请逐一检查此清单: 1. **阅读完整的错误信息**——Playwright 错误信息包含定位器、操作和页面状态的片段。答案往往就在细节中。 2. **启用 traces**——临时添加 `trace: 'on'` 或在配置中添加 `trace: 'on-first-retry'`。运行 `npx playwright show-trace trace.zip` 查看失败时的截图、DOM、网络和控制台信息。 3. **使用 Playwright Inspector**——运行 `npx playwright test --debug` 以交互方式逐步执行测试。 4. **检查定位器**——运行 `npx playwright codegen ` 验证定位器是否与实时页面匹配。 5. **检查 Playwright 版本**——运行 `npx playwright --version`。确保 `@playwright/test` 和浏览器二进制文件版本一致。 6. **搜索 Playwright 问题追踪器**——许多错误在 [github.com/microsoft/playwright/issues](https://github.com/microsoft/playwright/issues) 上有已知的解决方案。