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

26 KiB
Raw Permalink Blame History

配置

使用时机:新建 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",
    },
  },
})

模式 4globalSetup / 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

模式 6Trace、截图和视频设置

使用时机:决定本地开发与 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
在测试中硬编码 URLpage.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: 0CI 中 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"

原因:测试超过了其 timeoutPlaywright 在某个操作仍在运行时拆除了浏览器。

修复:不要增加全局超时。应使用 --trace on 找到慢步骤并修复它。常见原因:等待慢 API、未解决的网络请求或缺少 await

# 记录 trace 用于调试
npx playwright test --trace on
npx playwright show-report

相关