728 lines
26 KiB
Markdown
728 lines
26 KiB
Markdown
# 配置
|
||
|
||
> **使用时机**:新建 Playwright 项目、调整超时、添加浏览器目标、配置 CI 行为或连接环境特定设置时使用。
|
||
|
||
## 快速参考
|
||
|
||
```
|
||
npx playwright init # 脚手架生成配置 + 第一个测试
|
||
npx playwright test --config=custom.config.ts # 使用非默认配置
|
||
npx playwright test --project=chromium # 运行单个项目
|
||
npx playwright test --reporter=html # 覆盖报告器
|
||
npx playwright show-report # 打开最近的 HTML 报告
|
||
DEBUG=pw:api npx playwright test # 详细的 Playwright 日志
|
||
```
|
||
|
||
## 生产就绪配置(复制粘贴即用)
|
||
|
||
### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig, devices } from "@playwright/test"
|
||
import dotenv from "dotenv"
|
||
import path from "path"
|
||
|
||
// 从 .env 文件加载环境变量
|
||
dotenv.config({ path: path.resolve(__dirname, ".env") })
|
||
|
||
export default defineConfig({
|
||
// ── 测试发现 ──────────────────────────────────────────────
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
|
||
// ── 执行 ───────────────────────────────────────────────────
|
||
fullyParallel: true,
|
||
forbidOnly: !!process.env.CI, // 在 CI 中若有 test.only 则失败
|
||
retries: process.env.CI ? 2 : 0, // 仅在 CI 中重试不稳定测试
|
||
workers: process.env.CI ? "50%" : undefined, // CI 中只用一半 CPU,本地自动
|
||
|
||
// ── 报告 ───────────────────────────────────────────────────
|
||
reporter: process.env.CI
|
||
? [["html", { open: "never" }], ["github"]]
|
||
: [["html", { open: "on-failure" }]],
|
||
|
||
// ── 超时 ────────────────────────────────────────────────────
|
||
timeout: 30_000, // 每个测试的超时
|
||
expect: {
|
||
timeout: 5_000, // 每个断言的重试超时
|
||
},
|
||
|
||
// ── 共享浏览器上下文选项 ──────────────────────────────
|
||
use: {
|
||
baseURL: process.env.BASE_URL || "http://localhost:3000",
|
||
actionTimeout: 10_000, // click、fill 等
|
||
navigationTimeout: 15_000, // goto、waitForURL 等
|
||
|
||
// 产物收集
|
||
trace: "on-first-retry", // 仅在首次重试时记录完整 trace
|
||
screenshot: "only-on-failure", // 仅在失败时截图
|
||
video: "retain-on-failure", // 仅保留失败的视频
|
||
|
||
// 合理的默认值
|
||
locale: "en-US",
|
||
timezoneId: "America/New_York",
|
||
extraHTTPHeaders: {
|
||
"x-test-automation": "playwright",
|
||
},
|
||
},
|
||
|
||
// ── 项目(浏览器目标) ─────────────────────────────────
|
||
projects: [
|
||
{
|
||
name: "chromium",
|
||
use: { ...devices["Desktop Chrome"] },
|
||
},
|
||
{
|
||
name: "firefox",
|
||
use: { ...devices["Desktop Firefox"] },
|
||
},
|
||
{
|
||
name: "webkit",
|
||
use: { ...devices["Desktop Safari"] },
|
||
},
|
||
{
|
||
name: "mobile-chrome",
|
||
use: { ...devices["Pixel 7"] },
|
||
},
|
||
{
|
||
name: "mobile-safari",
|
||
use: { ...devices["iPhone 14"] },
|
||
},
|
||
],
|
||
|
||
// ── 开发服务器 ──────────────────────────────────────────────────
|
||
webServer: {
|
||
command: "npm run dev",
|
||
url: "http://localhost:3000",
|
||
reuseExistingServer: !process.env.CI,
|
||
timeout: 120_000, // 冷启动最多 2 分钟
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
},
|
||
})
|
||
```
|
||
|
||
### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig, devices } = require("@playwright/test")
|
||
const dotenv = require("dotenv")
|
||
const path = require("path")
|
||
|
||
dotenv.config({ path: path.resolve(__dirname, ".env") })
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
|
||
fullyParallel: true,
|
||
forbidOnly: !!process.env.CI,
|
||
retries: process.env.CI ? 2 : 0,
|
||
workers: process.env.CI ? "50%" : undefined,
|
||
|
||
reporter: process.env.CI
|
||
? [["html", { open: "never" }], ["github"]]
|
||
: [["html", { open: "on-failure" }]],
|
||
|
||
timeout: 30_000,
|
||
expect: {
|
||
timeout: 5_000,
|
||
},
|
||
|
||
use: {
|
||
baseURL: process.env.BASE_URL || "http://localhost:3000",
|
||
actionTimeout: 10_000,
|
||
navigationTimeout: 15_000,
|
||
|
||
trace: "on-first-retry",
|
||
screenshot: "only-on-failure",
|
||
video: "retain-on-failure",
|
||
|
||
locale: "en-US",
|
||
timezoneId: "America/New_York",
|
||
extraHTTPHeaders: {
|
||
"x-test-automation": "playwright",
|
||
},
|
||
},
|
||
|
||
projects: [
|
||
{
|
||
name: "chromium",
|
||
use: { ...devices["Desktop Chrome"] },
|
||
},
|
||
{
|
||
name: "firefox",
|
||
use: { ...devices["Desktop Firefox"] },
|
||
},
|
||
{
|
||
name: "webkit",
|
||
use: { ...devices["Desktop Safari"] },
|
||
},
|
||
{
|
||
name: "mobile-chrome",
|
||
use: { ...devices["Pixel 7"] },
|
||
},
|
||
{
|
||
name: "mobile-safari",
|
||
use: { ...devices["iPhone 14"] },
|
||
},
|
||
],
|
||
|
||
webServer: {
|
||
command: "npm run dev",
|
||
url: "http://localhost:3000",
|
||
reuseExistingServer: !process.env.CI,
|
||
timeout: 120_000,
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
},
|
||
})
|
||
```
|
||
|
||
## 模式
|
||
|
||
### 模式 1:环境特定配置
|
||
|
||
**使用时机**:测试针对开发、预发布和生产环境运行时。
|
||
**避免时机**:仅针对单一环境的本地项目。
|
||
|
||
#### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
import dotenv from "dotenv"
|
||
import path from "path"
|
||
|
||
// 加载环境特定的 .env 文件:.env.staging、.env.production 等
|
||
const ENV = process.env.TEST_ENV || "local"
|
||
dotenv.config({ path: path.resolve(__dirname, `.env.${ENV}`) })
|
||
|
||
const envConfig: Record<string, { baseURL: string; retries: number }> = {
|
||
local: { baseURL: "http://localhost:3000", retries: 0 },
|
||
staging: { baseURL: "https://staging.example.com", retries: 2 },
|
||
production: { baseURL: "https://www.example.com", retries: 2 },
|
||
}
|
||
|
||
const env = envConfig[ENV]
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
retries: env.retries,
|
||
use: {
|
||
baseURL: env.baseURL,
|
||
},
|
||
})
|
||
```
|
||
|
||
```bash
|
||
# 针对预发布环境运行
|
||
TEST_ENV=staging npx playwright test
|
||
|
||
# 针对生产环境运行(冒烟测试子集)
|
||
TEST_ENV=production npx playwright test --grep @smoke
|
||
```
|
||
|
||
#### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig } = require("@playwright/test")
|
||
const dotenv = require("dotenv")
|
||
const path = require("path")
|
||
|
||
const ENV = process.env.TEST_ENV || "local"
|
||
dotenv.config({ path: path.resolve(__dirname, `.env.${ENV}`) })
|
||
|
||
const envConfig = {
|
||
local: { baseURL: "http://localhost:3000", retries: 0 },
|
||
staging: { baseURL: "https://staging.example.com", retries: 2 },
|
||
production: { baseURL: "https://www.example.com", retries: 2 },
|
||
}
|
||
|
||
const env = envConfig[ENV]
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
retries: env.retries,
|
||
use: {
|
||
baseURL: env.baseURL,
|
||
},
|
||
})
|
||
```
|
||
|
||
### 模式 2:带设置依赖的多项目配置
|
||
|
||
**使用时机**:测试在运行前需要共享认证状态或数据库填充。
|
||
**避免时机**:测试完全独立,没有共享设置阶段。
|
||
|
||
#### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig, devices } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
|
||
projects: [
|
||
// 设置项目先运行,保存认证状态
|
||
{
|
||
name: "setup",
|
||
testMatch: /global\.setup\.ts/,
|
||
},
|
||
|
||
// 浏览器项目依赖于设置项目
|
||
{
|
||
name: "chromium",
|
||
use: {
|
||
...devices["Desktop Chrome"],
|
||
storageState: "playwright/.auth/user.json",
|
||
},
|
||
dependencies: ["setup"],
|
||
},
|
||
{
|
||
name: "firefox",
|
||
use: {
|
||
...devices["Desktop Firefox"],
|
||
storageState: "playwright/.auth/user.json",
|
||
},
|
||
dependencies: ["setup"],
|
||
},
|
||
],
|
||
})
|
||
```
|
||
|
||
```ts
|
||
// tests/global.setup.ts
|
||
import { test as setup, expect } from "@playwright/test"
|
||
|
||
const authFile = "playwright/.auth/user.json"
|
||
|
||
setup("authenticate", async ({ page }) => {
|
||
await page.goto("/login")
|
||
await page.getByLabel("Email").fill("user@example.com")
|
||
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!)
|
||
await page.getByRole("button", { name: "Sign in" }).click()
|
||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||
await page.context().storageState({ path: authFile })
|
||
})
|
||
```
|
||
|
||
#### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig, devices } = require("@playwright/test")
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
|
||
projects: [
|
||
{
|
||
name: "setup",
|
||
testMatch: /global\.setup\.js/,
|
||
},
|
||
{
|
||
name: "chromium",
|
||
use: {
|
||
...devices["Desktop Chrome"],
|
||
storageState: "playwright/.auth/user.json",
|
||
},
|
||
dependencies: ["setup"],
|
||
},
|
||
{
|
||
name: "firefox",
|
||
use: {
|
||
...devices["Desktop Firefox"],
|
||
storageState: "playwright/.auth/user.json",
|
||
},
|
||
dependencies: ["setup"],
|
||
},
|
||
],
|
||
})
|
||
```
|
||
|
||
```js
|
||
// tests/global.setup.js
|
||
const { test: setup, expect } = require("@playwright/test")
|
||
|
||
const authFile = "playwright/.auth/user.json"
|
||
|
||
setup("authenticate", async ({ page }) => {
|
||
await page.goto("/login")
|
||
await page.getByLabel("Email").fill("user@example.com")
|
||
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD)
|
||
await page.getByRole("button", { name: "Sign in" }).click()
|
||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||
await page.context().storageState({ path: authFile })
|
||
})
|
||
```
|
||
|
||
### 模式 3:带构建步骤的 `webServer`
|
||
|
||
**使用时机**:测试需要正在运行的应用程序服务器。让 Playwright 管理服务器生命周期。
|
||
**避免时机**:针对已部署的环境(预发布/生产)进行测试。
|
||
|
||
#### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
use: {
|
||
baseURL: "http://localhost:3000",
|
||
},
|
||
webServer: {
|
||
command: process.env.CI
|
||
? "npm run build && npm run start" // CI 中使用生产构建
|
||
: "npm run dev", // 本地使用开发服务器
|
||
url: "http://localhost:3000",
|
||
reuseExistingServer: !process.env.CI,
|
||
timeout: 120_000,
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
env: {
|
||
NODE_ENV: "test",
|
||
DATABASE_URL: process.env.DATABASE_URL || "postgresql://localhost:5432/test",
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
#### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig } = require("@playwright/test")
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
use: {
|
||
baseURL: "http://localhost:3000",
|
||
},
|
||
webServer: {
|
||
command: process.env.CI ? "npm run build && npm run start" : "npm run dev",
|
||
url: "http://localhost:3000",
|
||
reuseExistingServer: !process.env.CI,
|
||
timeout: 120_000,
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
env: {
|
||
NODE_ENV: "test",
|
||
DATABASE_URL: process.env.DATABASE_URL || "postgresql://localhost:5432/test",
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### 模式 4:`globalSetup` / `globalTeardown`
|
||
|
||
**使用时机**:一次性非浏览器工作:填充数据库、启动服务、设置环境变量。每次 `npx playwright test` 调用运行一次。
|
||
**避免时机**:需要浏览器上下文时(改用 setup 项目)或需要按测试隔离时(改用 fixtures)。
|
||
|
||
#### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
globalSetup: "./tests/global-setup.ts",
|
||
globalTeardown: "./tests/global-teardown.ts",
|
||
})
|
||
```
|
||
|
||
```ts
|
||
// tests/global-setup.ts
|
||
import { FullConfig } from "@playwright/test"
|
||
|
||
async function globalSetup(config: FullConfig) {
|
||
// 填充测试数据库
|
||
const { execSync } = await import("child_process")
|
||
execSync("npx prisma db seed", { stdio: "inherit" })
|
||
|
||
// 通过环境变量存储数据供测试使用
|
||
process.env.TEST_RUN_ID = `run-${Date.now()}`
|
||
}
|
||
|
||
export default globalSetup
|
||
```
|
||
|
||
```ts
|
||
// tests/global-teardown.ts
|
||
import { FullConfig } from "@playwright/test"
|
||
|
||
async function globalTeardown(config: FullConfig) {
|
||
const { execSync } = await import("child_process")
|
||
execSync("npx prisma db push --force-reset", { stdio: "inherit" })
|
||
}
|
||
|
||
export default globalTeardown
|
||
```
|
||
|
||
#### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig } = require("@playwright/test")
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
globalSetup: "./tests/global-setup.js",
|
||
globalTeardown: "./tests/global-teardown.js",
|
||
})
|
||
```
|
||
|
||
```js
|
||
// tests/global-setup.js
|
||
const { execSync } = require("child_process")
|
||
|
||
async function globalSetup(config) {
|
||
execSync("npx prisma db seed", { stdio: "inherit" })
|
||
process.env.TEST_RUN_ID = `run-${Date.now()}`
|
||
}
|
||
|
||
module.exports = globalSetup
|
||
```
|
||
|
||
```js
|
||
// tests/global-teardown.js
|
||
const { execSync } = require("child_process")
|
||
|
||
async function globalTeardown(config) {
|
||
execSync("npx prisma db push --force-reset", { stdio: "inherit" })
|
||
}
|
||
|
||
module.exports = globalTeardown
|
||
```
|
||
|
||
### 模式 5:`.env` 文件设置
|
||
|
||
**使用时机**:管理密钥、URL 或功能标记而不硬编码。
|
||
**避免时机**:切勿提交包含真实密钥的 `.env` 文件。请提供 `.env.example` 替代。
|
||
|
||
```bash
|
||
# .env.example(提交此文件)
|
||
BASE_URL=http://localhost:3000
|
||
TEST_PASSWORD=
|
||
API_KEY=
|
||
|
||
# .env.local(已 gitignore)
|
||
BASE_URL=http://localhost:3000
|
||
TEST_PASSWORD=s3cret
|
||
API_KEY=test-key-abc123
|
||
|
||
# .env.staging(已 gitignore)
|
||
BASE_URL=https://staging.example.com
|
||
TEST_PASSWORD=staging-password
|
||
API_KEY=staging-key-xyz789
|
||
```
|
||
|
||
```bash
|
||
# .gitignore
|
||
.env
|
||
.env.local
|
||
.env.staging
|
||
.env.production
|
||
playwright/.auth/
|
||
```
|
||
|
||
安装 dotenv:
|
||
|
||
```bash
|
||
npm install -D dotenv
|
||
```
|
||
|
||
### 模式 6:Trace、截图和视频设置
|
||
|
||
**使用时机**:决定本地开发与 CI 的产物收集策略。
|
||
|
||
| 设置 | 本地 | CI | 原因 |
|
||
| ------------ | ------------------------------- | ------------------------ | ----------------------------------------------- |
|
||
| `trace` | `'off'` 或 `'on-first-retry'` | `'on-first-retry'` | Trace 文件很大;仅在失败时收集 |
|
||
| `screenshot` | `'off'` | `'only-on-failure'` | 仅用于 CI 调试 |
|
||
| `video` | `'off'` | `'retain-on-failure'` | 视频录制速度慢;仅保留失败的视频 |
|
||
|
||
#### TypeScript
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
use: {
|
||
// CI:失败时全部捕获;本地:最小开销
|
||
trace: process.env.CI ? "on-first-retry" : "off",
|
||
screenshot: process.env.CI ? "only-on-failure" : "off",
|
||
video: process.env.CI ? "retain-on-failure" : "off",
|
||
},
|
||
})
|
||
```
|
||
|
||
#### JavaScript
|
||
|
||
```js
|
||
// playwright.config.js
|
||
const { defineConfig } = require("@playwright/test")
|
||
|
||
module.exports = defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.js",
|
||
use: {
|
||
trace: process.env.CI ? "on-first-retry" : "off",
|
||
screenshot: process.env.CI ? "only-on-failure" : "off",
|
||
video: process.env.CI ? "retain-on-failure" : "off",
|
||
},
|
||
})
|
||
```
|
||
|
||
## 决策指南
|
||
|
||
### 调整哪个超时
|
||
|
||
| 症状 | 应调整的超时 | 默认值 | 推荐范围 |
|
||
| --------------------------------------------------------------- | ------------------------- | ------------ | --------------------------- |
|
||
| 测试整体耗时过长 | `timeout` | 30s | 30-60s(切勿超过 120s) |
|
||
| 断言 `expect()` 不断重试,时间过长或不够 | `expect.timeout` | 5s | 5-10s |
|
||
| `page.goto()` 或 `waitForURL()` 超时 | `navigationTimeout` | 30s | 10-30s |
|
||
| `click()`、`fill()`、`check()` 超时 | `actionTimeout` | 0(无限制) | 10-15s |
|
||
| 开发服务器启动缓慢 | `webServer.timeout` | 60s | 60-180s |
|
||
|
||
### 服务器管理
|
||
|
||
| 场景 | 方式 | 原因 |
|
||
| --------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------- |
|
||
| 本地开发 + CI,应用在同一仓库 | `webServer` 配合 `reuseExistingServer: !process.env.CI` | Playwright 管理服务器;本地复用已有服务器 |
|
||
| 前后端分离仓库 | 手动启动或 Docker Compose | `webServer` 只能运行一条命令 |
|
||
| 测试已部署的预发布/生产环境 | 不使用 `webServer`;通过环境变量设置 `baseURL` | 服务器已在远程运行 |
|
||
| 需要多个服务(API + 前端) | `webServer` 条目数组 | 每个条目有自己的命令和 URL 健康检查 |
|
||
|
||
### 单项目 vs 多项目配置
|
||
|
||
| 场景 | 方式 | 原因 |
|
||
| --------------------------------------- | ---------------------------------------------------- | -------------------------------------------- |
|
||
| 起步阶段、早期开发 | 单项目(仅 chromium) | 反馈更快,配置更简单 |
|
||
| 预发布跨浏览器验证 | 多项目:chromium + firefox + webkit | 捕获渲染/API 差异 |
|
||
| 响应式移动端应用 | 在桌面项目基础上添加移动端项目 | 视口 + 触摸差异很重要 |
|
||
| 已认证 + 未认证测试 | 设置项目 + 依赖项目 | 共享认证状态,无需每个测试重新登录 |
|
||
| 时间预算紧张的 CI 流水线 | PR 检查中仅 chromium;合入 main 时运行所有浏览器 | 在速度与覆盖率之间取得平衡 |
|
||
|
||
### globalSetup vs 设置项目 vs Fixtures
|
||
|
||
| 需求 | 使用 | 原因 |
|
||
| ------------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
|
||
| 一次性数据库填充或外部服务准备 | `globalSetup` | 运行一次,无需浏览器 |
|
||
| 共享浏览器认证(登录一次,复用 cookie) | 带 `dependencies` 的设置项目 | 需要浏览器上下文;`globalSetup` 没有 |
|
||
| 按测试隔离状态(独立用户、全新数据) | 通过 `test.extend()` 的自定义 fixture | 每个测试拥有自己的实例,可清理 |
|
||
| 所有测试后的清理 | `globalTeardown` | 结束时运行一次,无论通过/失败 |
|
||
|
||
## 反模式
|
||
|
||
| 不要这样做 | 问题 | 应这样做 |
|
||
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||
| 全局设置 `timeout: 300_000` | 掩盖不稳定测试;CI 运行耗时漫长 | 修复根本原因;保持 timeout 为 30s;仅在必要时调高 `navigationTimeout` |
|
||
| 在测试中硬编码 URL:`page.goto('http://localhost:3000/login')` | 在非本地环境中全部失效 | 在配置中使用 `baseURL`,然后使用 `page.goto('/login')` |
|
||
| 在每个 PR 上都运行 chromium + firefox + webkit | 3 倍 CI 时间,对大多数 PR 收益甚微 | PR 上仅运行 chromium;合入 main 分支时运行所有浏览器 |
|
||
| 在 CI 中始终设置 `trace: 'on'` | 大量产物文件、上传慢、磁盘满 | `trace: 'on-first-retry'` —— 仅在测试失败并重试时捕获 |
|
||
| 在 CI 中始终设置 `video: 'on'` | CI 存储暴增;录制会拖慢测试 | `video: 'retain-on-failure'` —— 全部录制但仅保留失败的 |
|
||
| 在每个测试文件中内联配置:`test.use({ viewport: { width: 1280, height: 720 } })` | 分散各处,难以维护,不一致 | 在项目配置中定义一次;仅在确实需要时才按文件覆盖 |
|
||
| 本地设置 `retries: 3` | 开发期间掩盖不稳定问题 | 本地 `retries: 0`,CI 中 `retries: 2` |
|
||
| 在 CI 中未设置 `forbidOnly` | 意外提交 `test.only` 后只运行一个测试,其余全部静默跳过 | `forbidOnly: !!process.env.CI` |
|
||
| 使用 `globalSetup` 进行浏览器认证 | 无浏览器上下文;需要复杂的变通方法 | 使用带 `dependencies` 的设置项目 |
|
||
| 提交包含真实凭据的 `.env` 文件 | 安全风险 | 仅提交 `.env.example`;在 gitignore 中忽略真实 `.env` 文件 |
|
||
|
||
## 故障排查
|
||
|
||
### "baseURL" 不起作用——测试导航到了完整 URL
|
||
|
||
**原因**:使用了 `page.goto('http://localhost:3000/path')` 而不是 `page.goto('/path')`。当 `goto` 接收到绝对 URL 时,会忽略 `baseURL`。
|
||
|
||
**修复**:始终向 `page.goto()` 传递相对路径:
|
||
|
||
```ts
|
||
// 错误——忽略 baseURL
|
||
await page.goto("http://localhost:3000/dashboard")
|
||
|
||
// 正确——使用配置中的 baseURL
|
||
await page.goto("/dashboard")
|
||
```
|
||
|
||
### webServer 已启动,但测试仍因连接被拒绝而失败
|
||
|
||
**原因**:`webServer` 中的 `url` 与服务器实际提供的地址不匹配,或者健康检查端点返回非 200 状态码。
|
||
|
||
**修复**:确保 `webServer.url` 与实际服务器地址一致。如有需要,添加健康检查路由:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
webServer: {
|
||
command: "npm run dev",
|
||
url: "http://localhost:3000/api/health", // 使用真实端点
|
||
reuseExistingServer: !process.env.CI,
|
||
timeout: 120_000,
|
||
},
|
||
})
|
||
```
|
||
|
||
### 测试本地通过但在 CI 中超时
|
||
|
||
**原因**:CI 机器性能较慢。默认超时对 CI 硬件来说太紧。
|
||
|
||
**修复**:为 CI 增加 `navigationTimeout`,减少 `workers` 以避免资源争用:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
testDir: "./tests",
|
||
testMatch: "**/*.spec.ts",
|
||
workers: process.env.CI ? "50%" : undefined,
|
||
use: {
|
||
navigationTimeout: process.env.CI ? 30_000 : 15_000,
|
||
actionTimeout: process.env.CI ? 15_000 : 10_000,
|
||
},
|
||
})
|
||
```
|
||
|
||
### "Error: page.goto: Target page, context or browser has been closed"
|
||
|
||
**原因**:测试超过了其 `timeout`,Playwright 在某个操作仍在运行时拆除了浏览器。
|
||
|
||
**修复**:不要增加全局超时。应使用 `--trace on` 找到慢步骤并修复它。常见原因:等待慢 API、未解决的网络请求或缺少 `await`。
|
||
|
||
```bash
|
||
# 记录 trace 用于调试
|
||
npx playwright test --trace on
|
||
npx playwright show-report
|
||
```
|
||
|
||
## 相关
|
||
|
||
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) —— 替代 `globalSetup` 的自定义 fixture,用于按测试状态管理
|
||
- [core/test-organization.md](test-organization.md) —— 文件结构、命名约定、测试分组
|
||
- [core/authentication.md](authentication.md) —— 共享认证状态的设置项目
|
||
- [ci/ci-github-actions.md](../ci/ci-github-actions.md) —— CI 特定配置与缓存
|
||
- [ci/projects-and-dependencies.md](../ci/projects-and-dependencies.md) —— 高级多项目模式
|