项目文件夹

文件
2026-07-13 21:36:47 +08:00

728 行
26 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 配置
> **使用时机**:新建 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) —— 高级多项目模式