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

728 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 配置
> **使用时机**:新建 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) —— 高级多项目模式