关于 playwright-automation
playwright-automation 是面向编码 Agent 的 Playwright 测试编写指南,重点在 TypeScript 中构建稳定、易维护的端到端测试。它强调用户可见定位器、自动等待、隔离和 fixture,而非易波动的测试模式。
主要功能
- 01优先使用面向用户的定位器
- 02依靠自动等待,避免固定延时
- 03使用 BrowserContext 隔离测试
- 04通过 fixture 管理测试设置
- 05组织页面对象与可复用组件
适用场景
- 01为新项目搭建 Playwright 端到端测试
- 02逐步迁移易波动的 Cypress 或 Selenium 测试
- 03设计并行执行和 CI 分片策略
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill playwright-automation安装后在 Claude Code 中输入/playwright-automation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdauth-patterns.mdci-recipes.mddebugging-and-triage.mdfixtures-and-projects.mdmulti-site-architecture.mdnetwork-and-mocking.mdselector-strategies.md详细介绍
探索问题
先检查 .agents/qa-project-context.md——如果文件存在,就使用其中的信息,并跳过已回答的问题。然后只询问尚未解答的问题:
- 使用 TypeScript 还是 JavaScript? 强烈推荐 TypeScript——它能在编译时发现定位器和断言错误,而且此处的所有示例都以它为前提。
- 要测试哪些浏览器? 本地开发使用 Chromium;在 CI 中加入 Firefox 和 WebKit。移动视口应作为独立的 Playwright 项目,而不是独立的测试文件——它们会更改设备描述符。
- 已有测试套件还是从零开始? 从 Cypress/Selenium 迁移时,先重写最不稳定的测试;切勿一次性全面重写。这会彻底改变迁移的顺序。
- 单站点还是多站点? 多站点需要共享 fixture 和各站点的配置对象——参见
references/multi-site-architecture.md。
核心原则
- 优先使用面向用户的定位器。
getByRole>getByLabel>getByTestId> CSS(最后手段)。定位器必须反映用户看到的内容,而不是 DOM 的结构。参见references/selector-strategies.md。 - 自动等待——绝不使用
waitForTimeout。 每个 Playwright 操作和 Web 优先断言都会自动等待。如果你觉得需要超时,就应该改用更好的定位器或断言。 - 测试隔离。 每个测试都会获得一个全新的
BrowserContext,但这无法隔离服务端的账户、购物车、订单或密码变更。如果测试会修改共享状态,就分配独立记录/账户,或使用全新的数据库。测试不得依赖执行顺序。 - 默认并行,仅在必要时串行。 使用
fullyParallel: true。只有在流程确实无法隔离时(很少见),才使用test.describe.serial。 - 用 fixture 进行设置,而不是用钩子。 fixture 可以组合、提供类型安全保障,并自动清理。对于任何非简单的设置,优先使用 fixture,而不是
beforeEach/afterEach。参见references/fixtures-and-projects.md。
根据团队成熟度进行调整(在
.agents/qa-project-context.md中设置team_maturity):
- startup——仅使用 Chromium,编写 5–10 个关键路径测试,在 PR 上运行基础 CI。在测试套件稳定前,跳过分片和视觉基线。
- growing——使用 Chromium + Firefox,采用 POM 结构、并行执行、在 CI 中分片,并生成 HTML 报告制品。
- established——完整的浏览器矩阵、身份验证 fixture、API 模拟层、视觉回归基线、失败时记录 trace,以及不稳定测试跟踪。
项目结构
project-root/
├── playwright.config.ts
├── e2e/
│ ├── fixtures/ # base.fixture.ts, auth.fixture.ts, data.fixture.ts
│ ├── pages/ # Page objects by feature
│ │ ├── base.page.ts
│ │ ├── dashboard.page.ts
│ │ └── components/ # Reusable component objects (data-table, modal)
│ ├── tests/ # Test files by feature (auth/, dashboard/, settings/)
│ ├── helpers/ # test-data.ts, api-client.ts
│ └── global-setup.ts
├── .auth/ # Git-ignored storageState files
└── test-results/ # Git-ignored artifacts
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const isCI = !!process.env.CI;
const baseURL = process.env.BASE_URL ?? 'http://localhost:3000';
export default defineConfig({
testDir: './e2e', // Includes global-setup.ts as well as tests/.
testMatch: '**/*.spec.ts',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? '50%' : undefined,
reporter: isCI
? [['blob'], ['github'], ['json', { outputFile: 'test-results/results.json' }]]
: [['html', { open: 'on-failure' }]],
use: {
baseURL,
trace: isCI ? 'on-first-retry' : 'retain-on-failure',
screenshot: 'only-on-failure',
video: isCI ? 'on-first-retry' : 'off',
navigationTimeout: 30_000,
// Avoid a global actionTimeout — it can mask a genuinely slow auto-waited
// action. Set per-action only where a known-slow widget needs it.
},
projects: [
{ name: 'setup', testMatch: /global-setup\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
],
webServer: isCI ? undefined : {
command: 'npm run dev', url: baseURL, reuseExistingServer: !isCI, timeout: 120_000,
},
});
CI 中的 blob reporter 可合并分片运行结果;参见分片部分。setup 项目会在 Chromium 运行前写入 storageState。当受支持浏览器的要求需要时,请从 references/fixtures-and-projects.md 添加浏览器项目。只有在 testDir 中确实有清理测试时,才添加清理依赖。
全局设置(storageState)
import { test as setup, expect } from '@playwright/test';
setup('authenticate as default user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/.*dashboard/);
await page.context().storageState({ path: '.auth/user.json' });
});
这是 setup 项目模式:常规 Playwright 测试保存会话,依赖项目加载该会话。globalSetup 函数是另一种 API;请勿混用文件格式。共享身份验证适用于只读测试。会更改密码、账户设置或其他共享后端状态的测试需要使用独立账户或按测试进行配置。如需了解多角色身份验证和令牌预置,请参阅 references/auth-patterns.md。
编写大型测试套件之前,请运行 npx playwright test --list --project=chromium 和一个经过身份验证的冒烟测试。确认 setup 已被发现、实际应用 URL 正确,并且会使用已保存的会话。将 .auth/ 添加到 .gitignore;不要随报告发布 storage-state 文件。
页面对象模型
import { type Page, type Locator, expect } from '@playwright/test';
export abstract class BasePage {
constructor(protected readonly page: Page) {}
abstract readonly path: string;
async goto(): Promise<void> {
await this.page.goto(this.path);
await this.page.waitForLoadState('domcontentloaded');
}
}
组件对象表示可重复使用的 UI 片段(模态框、表格、导航)。它们接收根 Locator,而不是 Page:
export class DataTable {
readonly rows: Locator;
constructor(private readonly root: Locator) {
this.rows = root.getByRole('row');
}
getRowByText(text: string | RegExp): Locator {
return this.rows.filter({ hasText: text });
}
}
组合,而非深度继承。 页面持有其组件,而不是扩展五层继承层次结构:
export class UsersPage extends BasePage {
readonly path = '/admin/users';
readonly table: DataTable;
constructor(page: Page) {
super(page);
this.table = new DataTable(page.getByRole('table', { name: 'Users' }));
}
}
通过 fixture 注入页面对象,而不是在测试文件中通过构造函数创建:
export const test = base.extend<{ usersPage: UsersPage }>({
usersPage: async ({ page }, use) => { await use(new UsersPage(page)); },
});
export { expect } from '@playwright/test';
POM 方法返回状态(定位器、值),不执行断言。断言应放在测试中,这样失败时会指向测试,而不是页面对象。
测试模式
观察每次重要状态转换
验证持久化时,应在被测更改后立即重新加载,然后再执行其他操作,以免其他操作写入整个状态并掩盖保存失败。除了新增,也要测试移除和空状态。使用独立的预期值;单个汇总值可能掩盖错误。
如果某个操作会从筛选视图中移除其自身控件,应先建立初始状态,再执行操作,并断言稳定的结果列表/计数。复选框辅助函数可能会尝试检查已不存在的控件;对于这种状态转换,请使用常规定位器点击,并显式断言操作前后的状态。不要强制点击或吞掉超时。
当连贯的用户操作流程本身就是要验证的行为时,请用 test.step 将其放在一个测试中。不同测试应使用相互独立的初始数据。浏览器存储隔离不能替代服务器数据隔离,也不能证明会话已撤销。
使用 test.step 进行表单交互
将逻辑操作组包装在 test.step() 中,以便 trace viewer 输出更易读:
test('submits a multi-step form', async ({ page }) => {
await page.goto('/onboarding');
await test.step('fill personal info', async () => {
await page.getByLabel('First name').fill('Jane');
await page.getByRole('button', { name: 'Next' }).click();
});
await test.step('submit', async () => {
await page.getByRole('button', { name: 'Complete setup' }).click();
});
await expect(page).toHaveURL('/dashboard');
});
API 模拟
// Mock a response
await page.route('**/api/products*', async (route) => {
await route.fulfill({ json: { items: [{ id: '1', name: 'Widget', price: 29.99 }] } });
});
// Modify a real response
await page.route('**/api/feature-flags', async (route) => {
const response = await route.fetch();
const body = await response.json();
body.flags['new-checkout'] = true;
await route.fulfill({ response, json: body });
});
// Simulate an error
await page.route('**/api/products*', (route) => route.fulfill({ status: 500 }));
// WebSocket (v1.48+)
await page.routeWebSocket('**/ws/notifications', (ws) => {
ws.onMessage(() => ws.send(JSON.stringify({ type: 'alert', title: 'Deployed' })));
});
有关 HAR 回放和条件路由,请参阅 references/network-and-mocking.md。
经过身份验证的 APIRequestContext fixture
如需预置数据,或在不操作 UI 的情况下断言后端状态,请注入预先完成身份验证的 APIRequestContext。在 fixture 中获取令牌;切勿硬编码:
import { test, request, type APIRequestContext } from '@playwright/test';
// test.extend adds an `api` fixture to the base test object.
export const apiTest = test.extend<{ api: APIRequestContext }>({
api: async ({ baseURL }, use) => {
const ctx = await request.newContext({
baseURL,
extraHTTPHeaders: { Authorization: `Bearer ${process.env.API_TOKEN!}` },
});
await use(ctx);
await ctx.dispose();
},
});
标签和注解
test('checkout @smoke', async ({ page }) => { /* npx playwright test --grep @smoke */ });
test.slow(); // Triples timeout
test.skip(({ browserName }) => browserName === 'webkit', 'WebKit bug');
test.fixme('known issue tracked in JIRA-1234', async ({ page }) => { /* ... */ });
断言
始终优先使用 web-first 断言——它们会自动重试,直到条件成立或超时:
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('listitem')).toHaveText(['Apple', 'Banana', 'Cherry']);
软断言会收集所有失败,而不是在第一个失败处停止:
await expect.soft(page.getByLabel('Name')).toHaveValue('Jane Doe');
await expect.soft(page.getByLabel('Email')).toHaveValue('[email protected]');
ARIA 快照用于验证无障碍树结构并捕获语义回归:
await expect(page.getByRole('navigation', { name: 'Main' })).toMatchAriaSnapshot(`
- navigation "Main":
- link "Home"
- link "Products"
`);
视觉回归(单行示例;工作流另行介绍)
Playwright 内置的 toHaveScreenshot 会自动重试,并在首次运行时写入基线。遮罩动态区域;不要在它之前调用 waitForTimeout:
await expect(page.getByTestId('product-card')).toHaveScreenshot('product-card.png', {
mask: [page.getByTestId('price')],
});
有关基线管理、阈值(maxDiffPixelRatio、maskColor、stylePath)和审核工作流,请使用 visual-testing——视觉基线应放在该部分介绍。
无障碍扫描(axe;深入审计另行介绍)
上文的 ARIA 快照会检查结构,而非 WCAG 规则。如需基于规则的扫描,请添加 @axe-core/playwright:
import AxeBuilder from '@axe-core/playwright';
test('dashboard has no a11y violations', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
有关 WCAG 级别、规则调整和修复指导,请使用 accessibility-testing。
并行执行与 CI
在 CI 节点间分片
将测试套件拆分到矩阵任务中,然后将各分片的报告合并为一份 HTML 报告。达到 growing+ 成熟度后,分片才有意义;包含 5–10 个测试的 startup 套件不应分片。
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
每个分片都会上传 blob-report/;最后的任务会运行 npx playwright merge-reports --reporter=html ./all-blob-reports。blob 报告器(在上方配置中设置)是合并功能的关键——仅使用 --shard 会生成不完整的 HTML 报告。完整的 GitHub Actions 工作流、blob 上传/下载和构件模式,请参阅 references/ci-recipes.md。
调试
- Trace 查看器:
npx playwright show-trace test-results/.../trace.zip— 查看操作、网络、DOM 快照和控制台的时间线。 - UI 模式:
npx playwright test --ui— 实时逐步执行,并可时间回溯。 - 调试标志:
npx playwright test my-test.spec.ts --debug— 使用有头模式,并在每个操作处暂停。 - VS Code 扩展
ms-playwright.playwright— 从代码边栏运行/调试、选择定位器,并使用监视模式。 page.pause()会在测试运行过程中打开 Inspector。仅限本地使用——切勿提交。
关于不稳定测试的排查和构件分析,请参阅 references/debugging-and-triage.md。
已安装版本检查
选择与版本相关的 API 前,请读取锁文件并运行已安装的 CLI:
npm ls @playwright/test playwright
npx playwright --version
npx playwright test --help
确保测试包、浏览器安装和 CI 镜像使用相互兼容的固定版本。
对于项目尚未使用的 API,请查阅官方发行说明和
已安装的 TypeScript 声明。技能中的发行版本表无法确定用户的安装支持哪条命令。
迁移时,请使用 test-migration 并执行一个有代表性的迁移后测试;不要
臆造迁移命令。
实用功能包括用于控制浏览器时间的 page.clock、用于隔离 WebSocket 测试的 routeWebSocket、ARIA 快照,以及用于 CI 重试敏感门禁的 --fail-on-flaky-tests。
控制浏览器时钟不会推进服务器令牌过期时间或数据库时间。请检查
契约,并在服务器时间是变量时使用应用自有的测试时钟。
AI 增强编写(Test Agents 与 MCP)
有两种集成方式——根据 Agent 是在你的编辑器循环中运行,还是远程驱动真实浏览器来选择。
方式 A — Test Agents(npx playwright init-agents --loop=claude):构建 planner/generator/healer Agent,供编码 Agent 在其循环中加载。令牌效率高——无需 MCP 服务,也没有进程间通信开销。适用于“让 Claude/VS Code/opencode 替我编写 Playwright 测试”。
方式 B — @playwright/mcp:一个 MCP 服务,向任何兼容 MCP 的 Agent 提供浏览器操作。开销较高(进程边界、JSON 编组),但当 Agent 必须交互式地驱动实时浏览器,而不是离线编写测试时,这是正确的选择。配置:在 .mcp.json 中填写 { "mcpServers": { "playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] } } }。
关于修复测试失败,请参阅 test-reliability。关于从 PRD/规格首次生成测试,请参阅 ai-test-generation。
反模式
设计阶段的错误会悄然侵蚀测试套件。代码层面的“绝不做 X”清单位于 references/anti-patterns.md,其中包含 BAD/GOOD 对照——编写测试主体时请加载它。
1. 巨型页面对象
用一个类表示整个应用,会导致文件膨胀到 2000 行,每个测试都要导入它,而且无法安全重构。应按页面/功能拆分,并组合组件对象。
2. 会断言的 POM 方法
如果页面对象的方法调用 expect,就会让测试中的断言变得隐蔽。失败时,调用栈会指向页面对象,而非失败的场景。应返回定位器/状态;在测试中断言。
3. 对实现细节进行断言
以 CSS 类、DOM 嵌套结构或内部 ID 为依据的测试,会在每次重构时失效,即使实际行为没有变化。应断言用户能感知的内容——可见文本、角色、URL。
4. 依赖测试顺序的 fixture
会修改共享模块状态,或假设另一个测试已先运行的 fixture,一旦测试并行运行或独立运行就会失败。每个 fixture 都必须独立运行。
5. 明明可以使用 getByRole,却使用 data-testid
给已有可访问名称的按钮和标题添加测试 ID,会错过唾手可得、成本最低的无障碍信号。只有对没有稳定角色或标签的元素,才使用 getByTestId。
最具破坏性的运行时错误——使用 waitForTimeout 而不是自动等待的定位器来进行同步:
// BAD — slow on fast machines, flaky on slow ones, hides the real condition
await page.waitForTimeout(2000);
await page.click('#submit');
// GOOD — the action auto-waits for actionability
await page.getByRole('button', { name: 'Submit' }).click();
其他九种代码层面的错误做法(使用 CSS 而不是角色、使用 page.* 而不是定位器、force: true、共享状态、每个测试都登录、未检查稳定性就调用 locator.all()、使用 allTextContents() 而不是 toHaveText()、访问真实的第三方服务、提交 test.only)列于 references/anti-patterns.md。
验证
针对生成的产物运行以下命令,从最小范围开始:
npx playwright test --list # tests are discovered and parse
grep -rn 'waitForTimeout\|page.pause' e2e/ # must print nothing
npx tsc --noEmit # locator/assertion types compile
在 CI 中使用 eslint-plugin-playwright 强制执行“绝不做某事”规则——no-wait-for-timeout、no-force-option、no-element-handle、no-page-pause 规则会将本技能中的禁止事项转化为会导致 lint 失败的检查。
完成标准
- 存在
playwright.config.ts,其中包含至少针对 Chromium 的projects(面向 CI 时再添加 Firefox 和 WebKit),以及forbidOnly: !!process.env.CI。 - Page Object Model 文件位于
e2e/pages/(或等效目录)中,组件对象通过根Locator组合,且 POM 方法中没有expect。 grep -rn 'waitForTimeout' e2e/无任何结果,且已启用eslint-plugin-playwright的no-wait-for-timeout规则。- 每个定位器都使用
getByRole/getByLabel/getByTestId——grep -rn 'page.locator(\|xpath=\|css=' e2e/无任何结果(或只包含有注释说明的合理例外)。 - CI 会在 PR 上运行测试套件;成熟度达到
growing及以上时,会使用blobreporter 将测试分片到矩阵作业中,并添加merge-reports步骤,在失败时将 HTML 报告作为产物上传。
相关技能
- visual-testing — 截图基线创建、阈值调整以及审核/批准工作流。若要进行单个内联
toHaveScreenshot检查以外的操作,请参阅此技能。 - accessibility-testing — WCAG 等级、axe 规则调整以及修复。此技能只展示最简 axe 扫描。
- api-testing — 后端 API 验证、架构/契约测试,以及完整的
APIRequestContext用法模式。 - ci-cd-integration — Playwright 自带功能以外的流水线配置、并行化和报告。
- test-reliability — 对单个不稳定测试进行运行时修复(隔离、重试策略)。
- selector-drift-recovery — UI 重构导致多个测试失效后,离线批量重新生成选择器。
参考文件(位于 references/ 中)
| 文件 | 用途 |
|---|---|
anti-patterns.md | 涵盖每种代码层面错误做法的 BAD 与 GOOD 代码对照 |
fixtures-and-projects.md | Auth fixture、数据 fixture、多环境项目、组合 |
selector-strategies.md | 定位器决策树、getByRole 示例、稳定性评分 |
auth-patterns.md | storageState、多角色、令牌预置、会话过期 |
multi-site-architecture.md | 共享 fixture、站点级配置、单体仓库模式 |
network-and-mocking.md | page.route、route.fetch、HAR、WebSocket、条件路由 |
debugging-and-triage.md | Trace Viewer、不稳定测试分诊、重试、产物 |
ci-recipes.md | Reporters、分片与合并、--only-changed、浏览器缓存、Docker |