chore: import zh skill playwright-skill
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
# 调试 Playwright 测试
|
||||
|
||||
> **使用场景**:测试失败,你需要理解原因——选择器错误、时序问题、网络故障或意外的应用程序状态。
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 工具 | 命令 | 最适合的场景 |
|
||||
| -------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| UI 模式 | `npx playwright test --ui` | 交互式探索、可视化时间线、重新运行测试 |
|
||||
| Playwright Inspector | `PWDEBUG=1 npx playwright test` | 逐步调试、选择器试验场 |
|
||||
| Trace Viewer(追踪查看器) | `npx playwright show-trace trace.zip` | CI 失败的事后分析 |
|
||||
| 有头模式 | `npx playwright test --headed` | 在测试执行期间观察浏览器 |
|
||||
| 慢速模式 | `npx playwright test --headed --slow-mo=500` | 可视地跟踪快速交互 |
|
||||
| `page.pause()` | 插入测试代码中 | 在精确位置暂停以检查状态 |
|
||||
| 详细 API 日志 | `DEBUG=pw:api npx playwright test` | 查看每个 Playwright API 调用及其耗时 |
|
||||
| VS Code 扩展 | Playwright Test for VS Code | 断点、逐步执行、拾取定位器 |
|
||||
|
||||
## 系统化调试工作流
|
||||
|
||||
按此顺序执行。不要直接跳到第 5 步——大多数问题在第 2 步即可解决。
|
||||
|
||||
```
|
||||
1. 阅读完整的错误信息
|
||||
└─ 查阅 troubleshooting/error-index.md 了解已知模式
|
||||
2. 使用 --ui 运行以直观查看发生了什么
|
||||
└─ 时间线显示每个操作,失败点附有截图
|
||||
3. 如果尚未开启追踪,先启用
|
||||
└─ 在配置中临时使用 use: { trace: 'on' }
|
||||
4. 在追踪的网络标签页中检查 API 故障
|
||||
└─ 缺失的响应、4xx/5xx、CORS 错误
|
||||
5. 在失败点插入 page.pause()
|
||||
└─ 检查实时 DOM,在控制台中尝试选择器
|
||||
6. 检查浏览器控制台中的 JavaScript 错误
|
||||
└─ page.on('console') 或追踪中的控制台标签页
|
||||
```
|
||||
|
||||
## 模式
|
||||
|
||||
### 模式 1:使用 UI 模式进行交互式调试
|
||||
|
||||
**适用场景**:开发新测试、本地调查失败原因、探索应用程序行为。
|
||||
**避免场景**:CI 环境(改用追踪)。
|
||||
|
||||
UI 模式提供可视化时间线、每一步的 DOM 快照、网络瀑布图,以及重新运行单个测试的能力。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// 从终端启动 UI 模式:
|
||||
// npx playwright test --ui
|
||||
|
||||
// 在 UI 模式下运行特定测试文件:
|
||||
// npx playwright test tests/checkout.spec.ts --ui
|
||||
|
||||
// playwright.config.ts — 为 UI 模式便利性配置
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
use: {
|
||||
// 在 UI 模式下,追踪始终可用,不受此设置影响,
|
||||
// 但此设置确保 CI 失败时也能捕获追踪
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
// 从终端启动 UI 模式:
|
||||
// npx playwright test --ui
|
||||
|
||||
// 在 UI 模式下运行特定测试文件:
|
||||
// npx playwright test tests/checkout.spec.js --ui
|
||||
|
||||
// playwright.config.js
|
||||
const { defineConfig } = require("@playwright/test")
|
||||
|
||||
module.exports = defineConfig({
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 2:使用 PWDEBUG 的 Playwright Inspector
|
||||
|
||||
**适用场景**:需要逐步执行每个操作、交互式测试选择器,或查看每个操作前后的精确状态。
|
||||
**避免场景**:错误信息或追踪本身已能显示失败原因。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// 从终端启动 Inspector:
|
||||
// PWDEBUG=1 npx playwright test tests/login.spec.ts
|
||||
|
||||
// Windows PowerShell:
|
||||
// $env:PWDEBUG=1; npx playwright test tests/login.spec.ts
|
||||
|
||||
// Windows CMD:
|
||||
// set PWDEBUG=1 && npx playwright test tests/login.spec.ts
|
||||
|
||||
// Inspector 自动打开。使用以下控件:
|
||||
// - "Step over"(单步跳过)按钮:每次执行一个操作
|
||||
// - "Pick locator"(拾取定位器)按钮:悬停元素以查看最佳定位器
|
||||
// - "Resume"(继续)按钮:运行到下一个 page.pause() 或结束
|
||||
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("调试登录流程", async ({ page }) => {
|
||||
await page.goto("/login")
|
||||
|
||||
// 当 PWDEBUG=1 时,Inspector 会在每个操作前暂停
|
||||
await page.getByLabel("Email").fill("user@example.com")
|
||||
await page.getByLabel("Password").fill("password123")
|
||||
await page.getByRole("button", { name: "Sign in" }).click()
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
// 从终端启动 Inspector:
|
||||
// PWDEBUG=1 npx playwright test tests/login.spec.js
|
||||
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test("调试登录流程", async ({ page }) => {
|
||||
await page.goto("/login")
|
||||
|
||||
await page.getByLabel("Email").fill("user@example.com")
|
||||
await page.getByLabel("Password").fill("password123")
|
||||
await page.getByRole("button", { name: "Sign in" }).click()
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 3:使用 Trace Viewer 分析 CI 失败
|
||||
|
||||
**适用场景**:测试在 CI 中失败,你需要理解发生了什么,而无需在本地重新运行。
|
||||
**避免场景**:你能在本地复现失败(改用 UI 模式或 Inspector)。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts — 追踪配置
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
use: {
|
||||
// 'on-first-retry' — 仅在首次重试时捕获追踪(推荐用于 CI)
|
||||
// 'on' — 每次运行都捕获(临时用于顽固失败)
|
||||
// 'retain-on-failure' — 每次运行都捕获,仅保留失败的追踪
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
})
|
||||
|
||||
// CI 失败后,下载追踪产物并打开:
|
||||
// npx playwright show-trace test-results/tests-login-Login-test-chromium/trace.zip
|
||||
|
||||
// 或者通过 URL 打开:
|
||||
// npx playwright show-trace https://ci.example.com/artifacts/trace.zip
|
||||
|
||||
// 或者使用 trace.playwright.dev 在浏览器中查看追踪——拖放 zip 文件即可
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
// playwright.config.js
|
||||
const { defineConfig } = require("@playwright/test")
|
||||
|
||||
module.exports = defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
})
|
||||
|
||||
// CI 失败后,下载追踪产物并打开:
|
||||
// npx playwright show-trace test-results/tests-login-Login-test-chromium/trace.zip
|
||||
```
|
||||
|
||||
**阅读追踪——按顺序检查的内容:**
|
||||
|
||||
1. **Actions(操作)标签页**——查看每个 Playwright 操作及其前后截图
|
||||
2. **Console(控制台)标签页**——浏览器控制台输出(错误、警告、日志)
|
||||
3. **Network(网络)标签页**——每个 HTTP 请求及其状态、耗时、请求/响应体
|
||||
4. **Source(源码)标签页**——高亮显示失败行的测试源代码
|
||||
5. **Call(调用)标签页**——每个 Playwright 调用的精确参数和返回值
|
||||
|
||||
### 模式 4:有头模式加慢速
|
||||
|
||||
**适用场景**:希望观察浏览器执行过程,而不想承受完整的 Inspector 开销。
|
||||
**避免场景**:即使使用慢速模式,测试运行仍然太快而难以跟踪(改用 Inspector)。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// 从终端——快速可视化调试:
|
||||
// npx playwright test tests/checkout.spec.ts --headed --slow-mo=500
|
||||
|
||||
// playwright.config.ts — 为本地开发配置有头模式
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
use: {
|
||||
// 不要提交这些设置——改用 CLI 标志或环境变量检查
|
||||
headless: !process.env.HEADED,
|
||||
launchOptions: {
|
||||
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 然后运行:
|
||||
// HEADED=1 SLOW_MO=500 npx playwright test tests/checkout.spec.ts
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
// 从终端:
|
||||
// npx playwright test tests/checkout.spec.js --headed --slow-mo=500
|
||||
|
||||
// playwright.config.js
|
||||
const { defineConfig } = require("@playwright/test")
|
||||
|
||||
module.exports = defineConfig({
|
||||
use: {
|
||||
headless: !process.env.HEADED,
|
||||
launchOptions: {
|
||||
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 5:VS Code 集成
|
||||
|
||||
**适用场景**:你更喜欢基于 IDE 的调试,包括断点、变量检查和集成测试运行。
|
||||
**避免场景**:你在调试 CI 独有的失败,且该失败无法在本地复现。
|
||||
|
||||
安装 **Playwright Test for VS Code** 扩展(`ms-playwright.playwright`)。
|
||||
|
||||
**关键功能:**
|
||||
|
||||
- **运行/调试单个测试**——点击任意 `test()` 旁的绿色播放按钮
|
||||
- **设置断点**——点击左侧装订线设置断点;测试会自动在断点处暂停
|
||||
- **拾取定位器**——使用 "Pick locator" 命令悬停在元素上以获取最佳选择器
|
||||
- **显示浏览器**——在测试侧边栏中勾选 "Show Browser" 以在测试执行期间查看浏览器
|
||||
- **监听模式**——启用后可在保存文件时重新运行测试
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// 在 VS Code 中调试时,使用 test.only() 聚焦单个测试,
|
||||
// 而不是通过调试器运行整个套件
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test.only("调试这个特定测试", async ({ page }) => {
|
||||
await page.goto("/products")
|
||||
|
||||
// 在这一行设置 VS Code 断点,然后在调试面板中检查 `page`
|
||||
const productCard = page.getByRole("listitem").filter({ hasText: "Widget" })
|
||||
await expect(productCard).toBeVisible()
|
||||
|
||||
await productCard.getByRole("button", { name: "Add to cart" }).click()
|
||||
await expect(page.getByTestId("cart-count")).toHaveText("1")
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test.only("调试这个特定测试", async ({ page }) => {
|
||||
await page.goto("/products")
|
||||
|
||||
const productCard = page.getByRole("listitem").filter({ hasText: "Widget" })
|
||||
await expect(productCard).toBeVisible()
|
||||
|
||||
await productCard.getByRole("button", { name: "Add to cart" }).click()
|
||||
await expect(page.getByTestId("cart-count")).toHaveText("1")
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 6:捕获浏览器控制台日志
|
||||
|
||||
**适用场景**:怀疑存在 JavaScript 错误、客户端 API 调用失败,或应用程序级别的日志能解释失败原因。
|
||||
**避免场景**:问题显然是追踪中可见的选择器或时序问题。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("捕获控制台输出", async ({ page }) => {
|
||||
// 收集所有控制台消息
|
||||
const consoleLogs: string[] = []
|
||||
page.on("console", (msg) => {
|
||||
consoleLogs.push(`[${msg.type()}] ${msg.text()}`)
|
||||
})
|
||||
|
||||
// 捕获未捕获的异常
|
||||
page.on("pageerror", (error) => {
|
||||
console.error("页面错误:", error.message)
|
||||
})
|
||||
|
||||
await page.goto("/dashboard")
|
||||
await page.getByRole("button", { name: "Load data" }).click()
|
||||
await expect(page.getByRole("table")).toBeVisible()
|
||||
|
||||
// 失败时打印收集的日志以提供调试上下文
|
||||
console.log("浏览器控制台输出:", consoleLogs)
|
||||
})
|
||||
|
||||
// 用于所有测试的控制台日志记录可复用 fixture
|
||||
import { test as base } from "@playwright/test"
|
||||
|
||||
type ConsoleFixtures = {
|
||||
consoleMessages: string[]
|
||||
}
|
||||
|
||||
export const test = base.extend<ConsoleFixtures>({
|
||||
consoleMessages: async ({ page }, use) => {
|
||||
const messages: string[] = []
|
||||
page.on("console", (msg) => messages.push(`[${msg.type()}] ${msg.text()}`))
|
||||
page.on("pageerror", (err) => messages.push(`[pageerror] ${err.message}`))
|
||||
await use(messages)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test("捕获控制台输出", async ({ page }) => {
|
||||
const consoleLogs = []
|
||||
page.on("console", (msg) => {
|
||||
consoleLogs.push(`[${msg.type()}] ${msg.text()}`)
|
||||
})
|
||||
|
||||
page.on("pageerror", (error) => {
|
||||
console.error("页面错误:", error.message)
|
||||
})
|
||||
|
||||
await page.goto("/dashboard")
|
||||
await page.getByRole("button", { name: "Load data" }).click()
|
||||
await expect(page.getByRole("table")).toBeVisible()
|
||||
|
||||
console.log("浏览器控制台输出:", consoleLogs)
|
||||
})
|
||||
|
||||
// 用于控制台日志记录的可复用 fixture
|
||||
const { test: base } = require("@playwright/test")
|
||||
|
||||
const test = base.extend({
|
||||
consoleMessages: async ({ page }, use) => {
|
||||
const messages = []
|
||||
page.on("console", (msg) => messages.push(`[${msg.type()}] ${msg.text()}`))
|
||||
page.on("pageerror", (err) => messages.push(`[pageerror] ${err.message}`))
|
||||
await use(messages)
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = { test }
|
||||
```
|
||||
|
||||
### 模式 7:失败时截图
|
||||
|
||||
**适用场景**:你需要在失败发生的精确时刻获取可视化快照。
|
||||
**避免场景**:已启用追踪(追踪已经在每一步包含了截图)。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts — 失败时自动截图
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
use: {
|
||||
// 'off' — 不截图(默认)
|
||||
// 'on' — 每次测试后截图
|
||||
// 'only-on-failure' — 仅在测试失败时截图(推荐)
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 在特定点手动截图
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("调试视觉状态", async ({ page }) => {
|
||||
await page.goto("/checkout")
|
||||
await page.getByLabel("Promo code").fill("SAVE20")
|
||||
await page.getByRole("button", { name: "Apply" }).click()
|
||||
|
||||
// 在断言前捕获截图以进行调试
|
||||
await page.screenshot({ path: "test-results/before-discount.png", fullPage: true })
|
||||
|
||||
await expect(page.getByTestId("discount-amount")).toHaveText("-$20.00")
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
// playwright.config.js
|
||||
const { defineConfig } = require("@playwright/test")
|
||||
|
||||
module.exports = defineConfig({
|
||||
use: {
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 手动截图
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test("调试视觉状态", async ({ page }) => {
|
||||
await page.goto("/checkout")
|
||||
await page.getByLabel("Promo code").fill("SAVE20")
|
||||
await page.getByRole("button", { name: "Apply" }).click()
|
||||
|
||||
await page.screenshot({ path: "test-results/before-discount.png", fullPage: true })
|
||||
|
||||
await expect(page.getByTestId("discount-amount")).toHaveText("-$20.00")
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 8:网络调试
|
||||
|
||||
**适用场景**:怀疑 API 失败、请求负载错误、缺少认证头,或响应缓慢导致超时。
|
||||
**避免场景**:追踪的网络标签页已显示问题所在。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("调试网络请求", async ({ page }) => {
|
||||
// 记录所有请求
|
||||
page.on("request", (request) => {
|
||||
console.log(`>> ${request.method()} ${request.url()}`)
|
||||
})
|
||||
|
||||
// 记录所有响应及其状态码
|
||||
page.on("response", (response) => {
|
||||
console.log(`<< ${response.status()} ${response.url()}`)
|
||||
})
|
||||
|
||||
// 记录失败的请求(网络错误,而非 HTTP 错误)
|
||||
page.on("requestfailed", (request) => {
|
||||
console.log(`失败: ${request.url()} ${request.failure()?.errorText}`)
|
||||
})
|
||||
|
||||
await page.goto("/dashboard")
|
||||
await page.getByRole("button", { name: "Refresh" }).click()
|
||||
await expect(page.getByRole("table")).toBeVisible()
|
||||
})
|
||||
|
||||
// 等待特定 API 响应并检查它
|
||||
test("检查 API 响应", async ({ page }) => {
|
||||
await page.goto("/products")
|
||||
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) => resp.url().includes("/api/products") && resp.status() === 200
|
||||
)
|
||||
|
||||
await page.getByRole("button", { name: "Load products" }).click()
|
||||
|
||||
const response = await responsePromise
|
||||
const body = await response.json()
|
||||
console.log("API 响应:", JSON.stringify(body, null, 2))
|
||||
|
||||
await expect(page.getByRole("listitem")).toHaveCount(body.products.length)
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test("调试网络请求", async ({ page }) => {
|
||||
page.on("request", (request) => {
|
||||
console.log(`>> ${request.method()} ${request.url()}`)
|
||||
})
|
||||
|
||||
page.on("response", (response) => {
|
||||
console.log(`<< ${response.status()} ${response.url()}`)
|
||||
})
|
||||
|
||||
page.on("requestfailed", (request) => {
|
||||
console.log(`失败: ${request.url()} ${request.failure()?.errorText}`)
|
||||
})
|
||||
|
||||
await page.goto("/dashboard")
|
||||
await page.getByRole("button", { name: "Refresh" }).click()
|
||||
await expect(page.getByRole("table")).toBeVisible()
|
||||
})
|
||||
|
||||
test("检查 API 响应", async ({ page }) => {
|
||||
await page.goto("/products")
|
||||
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) => resp.url().includes("/api/products") && resp.status() === 200
|
||||
)
|
||||
|
||||
await page.getByRole("button", { name: "Load products" }).click()
|
||||
|
||||
const response = await responsePromise
|
||||
const body = await response.json()
|
||||
console.log("API 响应:", JSON.stringify(body, null, 2))
|
||||
|
||||
await expect(page.getByRole("listitem")).toHaveCount(body.products.length)
|
||||
})
|
||||
```
|
||||
|
||||
### 模式 9:详细 API 日志
|
||||
|
||||
**适用场景**:你需要查看每个 Playwright API 调用及其耗时,以确定测试在何处花费时间或卡住。
|
||||
**避免场景**:你已经知道哪个操作正在失败(改用 Inspector 或 `page.pause()`)。
|
||||
|
||||
```bash
|
||||
# 查看所有 Playwright API 调用及时间戳
|
||||
DEBUG=pw:api npx playwright test tests/slow-test.spec.ts
|
||||
|
||||
# 查看浏览器协议消息(非常详细——谨慎使用)
|
||||
DEBUG=pw:protocol npx playwright test tests/slow-test.spec.ts
|
||||
|
||||
# 组合多个调试通道
|
||||
DEBUG=pw:api,pw:browser npx playwright test tests/slow-test.spec.ts
|
||||
|
||||
# Windows PowerShell
|
||||
$env:DEBUG="pw:api"; npx playwright test tests/slow-test.spec.ts
|
||||
|
||||
# Windows CMD
|
||||
set DEBUG=pw:api && npx playwright test tests/slow-test.spec.ts
|
||||
```
|
||||
|
||||
### 模式 10:`page.pause()` — 内联断点
|
||||
|
||||
**适用场景**:你需要在精确位置暂停执行,以检查实时 DOM、尝试定位器或检查应用程序状态。
|
||||
**避免场景**:你可以使用 `PWDEBUG=1`,它会自动在每个步骤暂停。
|
||||
|
||||
**TypeScript**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("使用暂停进行调试", async ({ page }) => {
|
||||
await page.goto("/checkout")
|
||||
await page.getByLabel("Email").fill("user@example.com")
|
||||
|
||||
// 执行在此处暂停——Inspector 会打开,提供:
|
||||
// - 实时 DOM 检查
|
||||
// - 选择器试验场(在控制台中尝试定位器)
|
||||
// - 逐步执行剩余操作
|
||||
await page.pause()
|
||||
|
||||
// 这些操作会等待你在 Inspector 中点击 "Resume"
|
||||
await page.getByRole("button", { name: "Continue" }).click()
|
||||
await expect(page.getByText("Order confirmed")).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
**JavaScript**
|
||||
|
||||
```javascript
|
||||
const { test, expect } = require("@playwright/test")
|
||||
|
||||
test("使用暂停进行调试", async ({ page }) => {
|
||||
await page.goto("/checkout")
|
||||
await page.getByLabel("Email").fill("user@example.com")
|
||||
|
||||
// 执行在此处暂停——Inspector 会打开
|
||||
await page.pause()
|
||||
|
||||
await page.getByRole("button", { name: "Continue" }).click()
|
||||
await expect(page.getByText("Order confirmed")).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
**重要提示**:在提交前删除 `page.pause()`。它会在 CI 中无限挂起。使用此 lint 规则或 CI 检查:
|
||||
|
||||
```bash
|
||||
# 添加到 pre-commit 钩子或 CI 流水线
|
||||
grep -r "page.pause()" tests/ && echo "错误:提交前请移除 page.pause()" && exit 1
|
||||
```
|
||||
|
||||
## 决策指南
|
||||
|
||||
使用此表格根据失败类型选择正确的工具。
|
||||
|
||||
| 失败类型 | 首选工具 | 原因 |
|
||||
| ------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| **元素未找到**(选择器错误) | UI 模式(`--ui`) | 在失败时刻查看 DOM,在 Pick Locator 中尝试选择器 |
|
||||
| **元素未找到**(时序问题) | Trace Viewer — Actions(操作)标签页 | 比较前后截图,查看元素是否在超时后才出现 |
|
||||
| **文本/值错误** | Trace Viewer — Actions(操作)标签页 | 检查每个操作步骤的实际 DOM 内容 |
|
||||
| **测试挂起/超时** | `DEBUG=pw:api` | 查看哪个 API 调用正在等待且从未完成 |
|
||||
| **网络/API 失败** | Trace Viewer — Network(网络)标签页 | 查看请求/响应状态码、负载、耗时 |
|
||||
| **认证/会话问题** | 网络调试(`page.on('response')`) | 检查 401/403 响应、缺少的 cookie/token |
|
||||
| **视觉渲染错误** | `--headed --slow-mo=500` | 在浏览器中观察实际渲染 |
|
||||
| **应用中的 JavaScript 错误** | 控制台日志记录(`page.on('console')`) | 捕获未捕获的异常和错误日志 |
|
||||
| **仅 CI 失败** | Trace Viewer(来自 CI 产物) | 无需在本地运行即可重现精确的 CI 状态 |
|
||||
| **不稳定/间歇性失败** | 每次运行追踪(`trace: 'on'`)加重试 | 并排比较通过和失败的追踪 |
|
||||
| **状态污染** | 使用 `test.only()` 运行单个测试 | 与其他测试隔离;如果单独通过,则状态来自其他测试的泄漏 |
|
||||
|
||||
## 反模式
|
||||
|
||||
### 使用 `waitForTimeout` 修复时序问题
|
||||
|
||||
```typescript
|
||||
// 错误——任意延迟掩盖了真正的问题,使测试变得缓慢且不稳定
|
||||
await page.getByRole("button", { name: "Submit" }).click()
|
||||
await page.waitForTimeout(3000) // "加上这个延迟就能工作"
|
||||
await expect(page.getByText("Success")).toBeVisible()
|
||||
|
||||
// 正确——等待实际条件
|
||||
await page.getByRole("button", { name: "Submit" }).click()
|
||||
await expect(page.getByText("Success")).toBeVisible() // 自动重试最长 5 秒
|
||||
```
|
||||
|
||||
如果默认超时不够,请调查操作缓慢的_原因_,然后要么:
|
||||
|
||||
- 修复应用程序性能
|
||||
- 增加特定断言的超时时间:`await expect(locator).toBeVisible({ timeout: 15000 })`
|
||||
- 等待前置条件:`await page.waitForResponse('**/api/submit')`
|
||||
|
||||
### 注释掉测试以隔离失败
|
||||
|
||||
```typescript
|
||||
// 错误——注释掉测试以找出哪个导致失败
|
||||
// test('test A', ...);
|
||||
// test('test B', ...);
|
||||
test("test C — 这个会失败", async ({ page }) => {
|
||||
/* ... */
|
||||
})
|
||||
|
||||
// 正确——使用 .only 运行单个测试
|
||||
test.only("test C — 这个会失败", async ({ page }) => {
|
||||
/* ... */
|
||||
})
|
||||
|
||||
// 正确——使用 grep 运行匹配模式的测试
|
||||
// npx playwright test --grep "test C"
|
||||
```
|
||||
|
||||
### 不阅读完整的错误信息
|
||||
|
||||
Playwright 错误信息包括:
|
||||
|
||||
- **预期值**与**实际值**
|
||||
- 使用的**定位器**
|
||||
- **调用日志**,显示 Playwright 在超时前尝试的操作
|
||||
- 测试中的**行号**
|
||||
|
||||
阅读全部内容。仅凭调用日志通常就能精确显示问题所在(例如,元素存在但被隐藏时显示"等待选择器可见")。
|
||||
|
||||
### 在没有追踪的情况下调试 CI 失败
|
||||
|
||||
```typescript
|
||||
// 错误——CI 中没有追踪,无法调试失败
|
||||
export default defineConfig({
|
||||
use: {
|
||||
trace: "off",
|
||||
},
|
||||
})
|
||||
|
||||
// 正确——始终在 CI 失败时捕获追踪
|
||||
export default defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 使用 `console.log` 代替正确的调试工具
|
||||
|
||||
```typescript
|
||||
// 错误——到处散布 console.log
|
||||
test("使用日志调试", async ({ page }) => {
|
||||
await page.goto("/dashboard")
|
||||
console.log("页面已加载")
|
||||
const el = page.getByRole("button", { name: "Save" })
|
||||
console.log("按钮已找到:", await el.isVisible())
|
||||
console.log("按钮文本:", await el.textContent())
|
||||
// ...另外 20 个 console.log 调用
|
||||
|
||||
// 正确——在感兴趣的点使用 page.pause()
|
||||
await page.goto("/dashboard")
|
||||
await page.pause() // 交互式检查所有内容
|
||||
})
|
||||
```
|
||||
|
||||
### 提交包含 `page.pause()` 或 `test.only()` 的代码
|
||||
|
||||
```typescript
|
||||
// 错误——这些永远不应进入 CI
|
||||
test.only("聚焦测试", async ({ page }) => {
|
||||
// 跳过所有其他测试
|
||||
await page.goto("/")
|
||||
await page.pause() // 在 CI 中永远挂起
|
||||
})
|
||||
|
||||
// 如果在开发中需要,添加 CI 防护
|
||||
if (!process.env.CI) {
|
||||
await page.pause()
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 症状 | 可能原因 | 修复方法 |
|
||||
| ----------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Inspector 在 `PWDEBUG=1` 时不打开 | 在无头模式下运行,或 workers 大于 1 | 使用 `--headed` 和 `--workers=1` 运行 |
|
||||
| 追踪为空或缺失 | 配置中 `trace: 'off'`,或测试未重试 | 临时设置为 `trace: 'on'`,或 `trace: 'retain-on-failure'` |
|
||||
| UI 模式显示过时的测试结果 | 文件监听器未检测到更改 | 停止 UI 模式,清除 `test-results/`,重新启动 |
|
||||
| `page.pause()` 无效果 | 未设置 `PWDEBUG` 且在无头模式下运行 | 使用 `--headed` 运行或设置 `PWDEBUG=1` |
|
||||
| 截图空白或尺寸错误 | 未设置视口或测试在错误的浏览器项目中运行 | 在配置中设置 `viewport`;检查哪个浏览器项目运行了 |
|
||||
| 详细日志信息量过大 | 使用了 `DEBUG=pw:protocol` | 使用 `DEBUG=pw:api` 以获得可管理的详细级别 |
|
||||
| 追踪文件过大 | 对所有测试都开启 `trace: 'on'`,包括通过的测试 | 切换到 `trace: 'on-first-retry'` 或 `trace: 'retain-on-failure'` |
|
||||
| VS Code 未检测到测试 | `testDir` 或 `testMatch` 配置错误 | 确保配置路径匹配,且扩展设置指向你的 `playwright.config` |
|
||||
| 网络事件未触发 | 请求在监听器附加之前已发出 | 在 `page.goto()` 之前附加 `page.on('request')` 和 `page.on('response')` |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [core/error-index.md](error-index.md) — 查询特定错误信息
|
||||
- [core/flaky-tests.md](flaky-tests.md) — 间歇性失败模式及修复
|
||||
- [core/common-pitfalls.md](common-pitfalls.md) — 常见初学者错误
|
||||
- [core/assertions-and-waiting.md](assertions-and-waiting.md) — 以 Web 为先的断言和自动等待
|
||||
- [core/configuration.md](configuration.md) — 追踪、截图和重试配置
|
||||
- [ci/reporting-and-artifacts.md](../ci/reporting-and-artifacts.md) — 用于追踪和截图的 CI 产物收集配置
|
||||
Reference in New Issue
Block a user