26 KiB
配置
使用时机:新建 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
// 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
// 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
// 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,
},
})
# 针对预发布环境运行
TEST_ENV=staging npx playwright test
# 针对生产环境运行(冒烟测试子集)
TEST_ENV=production npx playwright test --grep @smoke
JavaScript
// 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
// 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"],
},
],
})
// 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
// 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"],
},
],
})
// 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
// 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
// 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
// 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",
})
// 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
// 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
// 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",
})
// 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
// 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 替代。
# .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
# .gitignore
.env
.env.local
.env.staging
.env.production
playwright/.auth/
安装 dotenv:
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
// 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
// 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() 传递相对路径:
// 错误——忽略 baseURL
await page.goto("http://localhost:3000/dashboard")
// 正确——使用配置中的 baseURL
await page.goto("/dashboard")
webServer 已启动,但测试仍因连接被拒绝而失败
原因:webServer 中的 url 与服务器实际提供的地址不匹配,或者健康检查端点返回非 200 状态码。
修复:确保 webServer.url 与实际服务器地址一致。如有需要,添加健康检查路由:
// 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 以避免资源争用:
// 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。
# 记录 trace 用于调试
npx playwright test --trace on
npx playwright show-report
相关
- core/fixtures-and-hooks.md —— 替代
globalSetup的自定义 fixture,用于按测试状态管理 - core/test-organization.md —— 文件结构、命名约定、测试分组
- core/authentication.md —— 共享认证状态的设置项目
- ci/ci-github-actions.md —— CI 特定配置与缓存
- ci/projects-and-dependencies.md —— 高级多项目模式