# 配置 > **使用时机**:新建 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 = { 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) —— 高级多项目模式