Files
2026-07-13 21:36:47 +08:00

58 KiB
Raw Permalink Blame History

Playwright 错误索引

针对特定 Playwright 错误消息的快速参考手册。找到你的错误,了解原因,应用修复。

如何使用:在此文件中搜索你在终端或测试报告中看到的确切错误文本。每条记录都给出了原因和可行的修复方案。


定位器与元素错误


"locator.click: Target closed"

原因Playwright 完成对元素的操作之前,页面或框架发生了导航跳转(或已被关闭)。

常见触发场景

  • 点击触发导航的链接时,另一个操作仍在等待中
  • 表单提交导致页面重新加载,而后续的 click()fill() 尚未完成
  • finally 块中调用了 page.close()context.close(),与正在执行的操作产生竞态
  • 应用程序中未处理的异常触发了错误页面的重定向

修复:在执行下一个操作之前等待导航完成,或者使用 Promise.all 协调点击和导航。

// TypeScript — 等待由点击链接引起的导航完成
await Promise.all([
  page.waitForURL("**/dashboard"),
  page.getByRole("link", { name: "Dashboard" }).click(),
])
// JavaScript — 相同的模式
await Promise.all([
  page.waitForURL("**/dashboard"),
  page.getByRole("link", { name: "Dashboard" }).click(),
])

如果页面是有意关闭的(例如弹窗),请在操作之前捕获引用:

// 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 — 处理自动关闭的弹窗
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.mdcore/multi-context-and-popups.md


"waiting for locator('...') to be visible"

原因Playwright 的自动等待超时,因为元素从未出现在 DOM 中或一直处于隐藏状态(例如 display: nonevisibility: hidden、零尺寸或位于 aria-hidden 之后)。

常见触发场景

  • 元素是条件渲染的,但条件未满足(缺少数据、未认证状态)
  • 内容是异步加载的,定位器在 API 响应返回之前就被执行了
  • 选择器错误——角色、名称或测试 ID 中有拼写错误
  • 元素存在但在屏幕之外或位于折叠的容器内
  • CSS 动画或过渡使元素保持 opacity: 0,而 Playwright 尚未判定为可见

修复:首先使用 Playwright Inspector 确认定位器与你期望的元素匹配。然后确保元素出现的先决条件已满足。

// 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 — 相同的做法
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.mdcore/assertions-and-waiting.md


"locator.click: Error: strict mode violation"

原因:定位器匹配了多个元素,Playwright 的严格模式(默认启用)拒绝在模棱两可的匹配上执行操作。

常见触发场景

  • 页面上存在多个按钮时使用 getByRole('button')
  • "Save" 和 "Save as draft" 同时存在时使用 getByText('Save')
  • 需要 exact: true 时使用了部分文本匹配
  • 同一个组件被渲染了多次(例如在列表中)

修复:使定位器更加具体,确保解析到唯一一个元素。

// 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

// 不好——匹配了多个按钮
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.mdcore/locator-strategy.md


"Error: expect(locator).toBeVisible() — locator resolved to X elements"

原因:与上述严格模式违规的根因相同——断言的定位器匹配了多个元素,因此 Playwright 无法确定要对哪个元素进行断言。

常见触发场景

  • 与上述严格模式违规的触发场景相同
  • 当存在多个 .item 元素时写 expect(page.locator('.item')).toBeVisible()

修复:将定位器缩小到单个元素(参见上方严格模式违规的修复方法)。或者,如果你有意要检查所有匹配:

// 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 — 断言数量而非可见性
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.mdcore/assertions-and-waiting.md


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