playwright-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<objective>
How an expert agent writes stable, maintainable, production-grade Playwright tests in TypeScript.
</objective>
<objective>
专家级Agent如何编写稳定、可维护的生产级TypeScript版Playwright测试。
</objective>
Discovery Questions
探索问题
Before generating any code, ask:
- TypeScript or JavaScript? TypeScript is strongly recommended. It catches locator and assertion mistakes at compile time, and every example in this skill assumes TypeScript.
- Which browsers? Chromium for local dev. Add Firefox and WebKit in CI. Mobile viewports are separate Playwright projects, not separate test files.
- Existing suite or fresh start? If migrating from Cypress or Selenium, start by rewriting the flakiest tests first. Do not attempt a big-bang rewrite.
- Single site or multi-site? Multi-site architectures need shared fixtures and per-site config objects. See .
references/multi-site-architecture.md
在生成任何代码之前,请先询问:
- 使用TypeScript还是JavaScript? 强烈推荐TypeScript。它能在编译阶段捕获定位器和断言错误,本技能中的所有示例均默认使用TypeScript。
- 测试哪些浏览器? 本地开发使用Chromium。CI环境中添加Firefox和WebKit。移动端视口作为独立的Playwright项目,而非单独的测试文件。
- 已有测试套件还是从零开始? 如果从Cypress或Selenium迁移,优先重写最不稳定的测试。切勿尝试一次性全部重写。
- 单站点还是多站点架构? 多站点架构需要共享fixtures和每个站点的配置对象。详见。
references/multi-site-architecture.md
Core Principles
核心原则
- User-facing locators first. >
getByRole>getByLabel> CSS (last resort). Locators must reflect what the user sees, not how the DOM is structured. SeegetByTestId.references/selector-strategies.md - Auto-waiting -- NEVER use . Every Playwright action and web-first assertion auto-waits. If you think you need a timeout, you need a better locator or assertion.
waitForTimeout - Test isolation. Each test gets a fresh . Tests must never depend on other tests' state or execution order.
BrowserContext - Parallel by default, serial only when necessary. Use in config. Reserve
fullyParallel: truefor flows that genuinely cannot be isolated (rare).test.describe.serial - Fixtures for setup, not hooks. Fixtures compose, provide type safety, and automatically tear down. Prefer them over /
beforeEachfor anything non-trivial. SeeafterEach.references/fixtures-and-projects.md
Calibrate to your team maturity (setinteam_maturity):.agents/qa-project-context.md
- startup — Chromium only, 5–10 critical path tests, basic CI run on PR. Skip sharding and visual testing until the suite is stable.
- growing — Multi-browser (Chromium + Firefox), POM structure, parallel execution, CI with sharding, HTML report artifacts.
- established — Full browser matrix, auth fixtures, API mocking layer, visual regression baseline, trace-on-failure, flakiness tracking.
- 优先使用面向用户的定位器。>
getByRole>getByLabel> CSS(最后选择)。定位器必须反映用户看到的内容,而非DOM的结构。详见getByTestId。references/selector-strategies.md - 自动等待——绝不使用。Playwright的每个操作和Web优先断言都会自动等待。如果你认为需要超时,说明你需要更好的定位器或断言。
waitForTimeout - 测试隔离。每个测试都会获得全新的。测试绝不能依赖其他测试的状态或执行顺序。
BrowserContext - 默认并行执行,仅在必要时串行。在配置中设置。仅对确实无法隔离的流程(极少情况)使用
fullyParallel: true。test.describe.serial - 用fixtures做初始化,而非钩子函数。Fixtures可组合、提供类型安全,且会自动清理。对于非 trivial 的初始化,优先使用fixtures而非/
beforeEach。详见afterEach。references/fixtures-and-projects.md
根据团队成熟度调整(在中设置.agents/qa-project-context.md):team_maturity
- 初创团队 —— 仅使用Chromium,编写5–10个关键路径测试,PR时运行基础CI。在测试套件稳定前,跳过分片和可视化测试。
- 成长型团队 —— 多浏览器(Chromium + Firefox)、POM结构、并行执行、带分片的CI、HTML报告产物。
- 成熟团队 —— 全浏览器矩阵、认证fixtures、API模拟层、视觉回归基准、失败时记录追踪信息、不稳定测试追踪。
Common AI Agent Mistakes
AI Agent常见错误
Do not generate code that matches any of these patterns.
请勿生成符合以下任何模式的代码。
1. Never use waitForTimeout()
as synchronization
waitForTimeout()1. 绝不要用waitForTimeout()
做同步操作
waitForTimeout()Why it is wrong: Arbitrary waits are slow on fast machines and flaky on slow ones. They hide the real condition you are waiting for.
typescript
// BAD
await page.waitForTimeout(2000);
await page.click('#submit');
// GOOD
await page.getByRole('button', { name: 'Submit' }).click(); // auto-waits错误原因:任意等待在快机器上会变慢,在慢机器上会不稳定。它会掩盖你真正需要等待的条件。
typescript
// BAD
await page.waitForTimeout(2000);
await page.click('#submit');
// GOOD
await page.getByRole('button', { name: 'Submit' }).click(); // 自动等待2. Never default to CSS/XPath when getByRole
/getByLabel
/getByTestId
work
getByRolegetByLabelgetByTestId2. 当getByRole
/getByLabel
/getByTestId
可用时,绝不要默认使用CSS/XPath
getByRolegetByLabelgetByTestIdWhy it is wrong: CSS selectors encode DOM structure, break on refactors, and do not communicate test intent.
typescript
// BAD
await page.locator('.btn-primary.submit-form').click();
// GOOD
await page.getByRole('button', { name: 'Submit' }).click();See the full decision tree in .
references/selector-strategies.md错误原因:CSS选择器编码了DOM结构,重构时会失效,且无法传达测试意图。
typescript
// BAD
await page.locator('.btn-primary.submit-form').click();
// GOOD
await page.getByRole('button', { name: 'Submit' }).click();详见中的完整决策树。
references/selector-strategies.md3. Never use discouraged page.*
APIs when locator APIs exist
page.*3. 当定位器API存在时,绝不要使用不推荐的page.*
API
page.*Why it is wrong: , , are legacy convenience methods that bypass the locator auto-waiting pipeline and cannot be chained or filtered.
page.click()page.fill()page.type()typescript
// BAD
await page.click('#email');
await page.fill('#email', 'user@example.com');
// GOOD
await page.getByLabel('Email').fill('user@example.com');错误原因:、、是遗留的便捷方法,会绕过定位器的自动等待流程,且无法链式调用或过滤。
page.click()page.fill()page.type()typescript
// BAD
await page.click('#email');
await page.fill('#email', 'user@example.com');
// GOOD
await page.getByLabel('Email').fill('user@example.com');4. Never use force: true
without documented justification
force: true4. 绝不要无文档说明地使用force: true
force: trueWhy it is wrong: skips actionability checks (visible, enabled, stable, receives events). Either the wrong element is targeted, or there is an accessibility bug.
force: truetypescript
// BAD
await page.getByRole('button', { name: 'Save' }).click({ force: true });
// GOOD -- dismiss any overlay first
await page.getByRole('button', { name: 'Dismiss' }).click();
await page.getByRole('button', { name: 'Save' }).click();错误原因:会跳过可操作性检查(可见、启用、稳定、可接收事件)。要么是定位了错误元素,要么存在可访问性缺陷。
force: truetypescript
// BAD
await page.getByRole('button', { name: 'Save' }).click({ force: true });
// GOOD -- 先关闭任何浮层
await page.getByRole('button', { name: 'Dismiss' }).click();
await page.getByRole('button', { name: 'Save' }).click();5. Never share mutable state between tests
5. 绝不要在测试之间共享可变状态
Why it is wrong: Tests run in parallel. Shared module-level variables create race conditions and order-dependent failures. Use fixtures instead. See .
references/anti-patterns.md错误原因:测试并行运行。共享模块级变量会导致竞态条件和依赖执行顺序的失败。改用fixtures。详见。
references/anti-patterns.md6. Never put login boilerplate in every test -- use storageState
storageState6. 绝不要在每个测试中重复登录代码——使用storageState
storageStateWhy it is wrong: UI login for every test is slow and fragile. logs in once and replays cookies/localStorage for all tests. See .
storageStatereferences/auth-patterns.md错误原因:每个测试都通过UI登录会很慢且不稳定。只需登录一次,然后为所有测试重放cookie/localStorage。详见。
storageStatereferences/auth-patterns.md7. Never use locator.all()
on dynamic collections without a stability check
locator.all()7. 绝不要在动态集合上使用locator.all()
而不做稳定性检查
locator.all()Why it is wrong: returns a snapshot. If the DOM is still updating, you get a partial or empty array. It does not auto-retry.
locator.all()typescript
// BAD
const items = await page.getByRole('listitem').all();
expect(items.length).toBe(5); // may be 0 if DOM is still rendering
// GOOD
await expect(page.getByRole('listitem')).toHaveCount(5);
const items = await page.getByRole('listitem').all(); // then iterate if needed错误原因:返回快照。如果DOM仍在更新,你会得到部分或空数组。它不会自动重试。
locator.all()typescript
// BAD
const items = await page.getByRole('listitem').all();
expect(items.length).toBe(5); // 如果DOM仍在渲染,可能为0
// GOOD
await expect(page.getByRole('listitem')).toHaveCount(5);
const items = await page.getByRole('listitem').all(); // 如有需要再遍历8. Never assert with allTextContents()
when toHaveText()
gives retryability
allTextContents()toHaveText()8. 当toHaveText()
可提供重试功能时,绝不要用allTextContents()
做断言
toHaveText()allTextContents()Why it is wrong: is a snapshot that does not retry. retries until the condition is met or timeout expires.
allTextContents()toHaveText()typescript
// BAD
const texts = await page.getByRole('listitem').allTextContents();
expect(texts).toEqual(['Apple', 'Banana', 'Cherry']);
// GOOD
await expect(page.getByRole('listitem')).toHaveText(['Apple', 'Banana', 'Cherry']);错误原因:是快照,不会重试。会重试直到条件满足或超时。
allTextContents()toHaveText()typescript
// BAD
const texts = await page.getByRole('listitem').allTextContents();
expect(texts).toEqual(['Apple', 'Banana', 'Cherry']);
// GOOD
await expect(page.getByRole('listitem')).toHaveText(['Apple', 'Banana', 'Cherry']);9. Never test external dependencies you do not control
9. 绝不要测试你无法控制的外部依赖
Why it is wrong: Third-party services have their own uptime, rate limits, and UI changes. Tests that hit real external services are flaky by definition.
typescript
// BAD -- hitting real Stripe checkout
await page.goto('https://checkout.stripe.com/...');
// GOOD -- mock the external integration
await page.route('**/api/create-checkout-session', async (route) => {
await route.fulfill({ json: { sessionId: 'mock_session', url: '/success' } });
});错误原因:第三方服务有自己的可用性、速率限制和UI变更。访问真实外部服务的测试本质上是不稳定的。
typescript
// BAD -- 访问真实的Stripe结账页
await page.goto('https://checkout.stripe.com/...');
// GOOD -- 模拟外部集成
await page.route('**/api/create-checkout-session', async (route) => {
await route.fulfill({ json: { sessionId: 'mock_session', url: '/success' } });
});10. Never leave test.only
in committed code
test.only10. 绝不要在提交的代码中保留test.only
test.onlyWhy it is wrong: A single silently skips every other test in the suite. In CI, you run one test and think everything passes.
test.onlytypescript
export default defineConfig({ forbidOnly: !!process.env.CI });错误原因:单个会静默跳过套件中的所有其他测试。在CI中,你只运行了一个测试,却以为所有测试都通过了。
test.onlytypescript
export default defineConfig({ forbidOnly: !!process.env.CI });Project Structure
项目结构
project-root/
├── playwright.config.ts
├── e2e/
│ ├── fixtures/ # base.fixture.ts, auth.fixture.ts, data.fixture.ts
│ ├── pages/ # Page objects organized by feature
│ │ ├── base.page.ts
│ │ ├── dashboard.page.ts
│ │ └── components/ # Reusable component objects
│ │ ├── data-table.component.ts
│ │ └── modal.component.ts
│ ├── tests/ # Test files organized by feature
│ │ ├── auth/
│ │ ├── dashboard/
│ │ └── settings/
│ ├── helpers/ # test-data.ts, api-client.ts
│ └── global-setup.ts
├── .auth/ # Git-ignored storageState files
└── test-results/ # Git-ignored artifactsproject-root/
├── playwright.config.ts
├── e2e/
│ ├── fixtures/ # base.fixture.ts, auth.fixture.ts, data.fixture.ts
│ ├── pages/ # 按功能组织的页面对象
│ │ ├── base.page.ts
│ │ ├── dashboard.page.ts
│ │ └── components/ # 可复用组件对象
│ │ ├── data-table.component.ts
│ │ └── modal.component.ts
│ ├── tests/ # 按功能组织的测试文件
│ │ ├── auth/
│ │ ├── dashboard/
│ │ └── settings/
│ ├── helpers/ # test-data.ts, api-client.ts
│ └── global-setup.ts
├── .auth/ # Git忽略的storageState文件
└── test-results/ # Git忽略的产物playwright.config.ts
playwright.config.ts
typescript
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/tests',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? '50%' : undefined,
reporter: isCI
? [['html', { open: 'never' }], ['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',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
projects: [
{ name: 'setup', testMatch: /global-setup\.ts/, teardown: 'teardown' },
{ name: 'teardown', testMatch: /global-teardown\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
{ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
{ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
],
webServer: isCI ? undefined : {
command: 'npm run dev', url: baseURL, reuseExistingServer: true, timeout: 120_000,
},
});typescript
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/tests',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? '50%' : undefined,
reporter: isCI
? [['html', { open: 'never' }], ['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',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
projects: [
{ name: 'setup', testMatch: /global-setup\.ts/, teardown: 'teardown' },
{ name: 'teardown', testMatch: /global-teardown\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
{ name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
{ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
],
webServer: isCI ? undefined : {
command: 'npm run dev', url: baseURL, reuseExistingServer: true, timeout: 120_000,
},
});Global Setup
全局初始化
typescript
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' });
});typescript
import { test as setup, expect } from '@playwright/test';
setup('以默认用户身份认证', 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' });
});Page Object Model
页面对象模型
Base Page
基础页面
typescript
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.waitForReady();
}
async waitForReady(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
}
}typescript
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.waitForReady();
}
async waitForReady(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
}
}Component Objects
组件对象
Component objects represent reusable UI fragments (modals, data tables, nav bars). They take a root , not a .
LocatorPagetypescript
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 });
}
}组件对象代表可复用的UI片段(模态框、数据表、导航栏)。它们接收根,而非。
LocatorPagetypescript
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 });
}
}Fixture-Based Injection
基于Fixture的注入
Inject page objects via fixtures, not constructors in test files.
typescript
export const test = base.extend<{ dashboardPage: DashboardPage }>({
dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)); },
});
export { expect } from '@playwright/test';通过fixtures注入页面对象,而非在测试文件中使用构造函数。
typescript
export const test = base.extend<{ dashboardPage: DashboardPage }>({
dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)); },
});
export { expect } from '@playwright/test';Composition Over Inheritance
组合优于继承
Compose component objects rather than inherit from deep class hierarchies.
typescript
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' }));
}
}组合组件对象,而非从深层类继承。
typescript
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' }));
}
}Test Patterns
测试模式
Authentication (storageState reuse)
认证(storageState复用)
Global setup logs in once and saves . All test projects load it via config. For multi-role auth (admin/user/guest), see .
storageStatereferences/auth-patterns.md全局初始化登录一次并保存。所有测试项目通过配置加载它。如需多角色认证(管理员/普通用户/访客),详见。
storageStatereferences/auth-patterns.mdForm Interactions with test.step
用test.step处理表单交互
Wrap logical action groups in for better trace viewer output.
test.step()typescript
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');
});将逻辑操作组包裹在中,以获得更好的追踪查看器输出。
test.step()typescript
test('提交多步骤表单', async ({ page }) => {
await page.goto('/onboarding');
await test.step('填写个人信息', async () => {
await page.getByLabel('First name').fill('Jane');
await page.getByRole('button', { name: 'Next' }).click();
});
await test.step('提交表单', async () => {
await page.getByRole('button', { name: 'Complete setup' }).click();
});
await expect(page).toHaveURL('/dashboard');
});API Mocking
API模拟
typescript
// 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 errors
await page.route('**/api/products*', (route) => route.fulfill({ status: 500 }));
// WebSocket (v1.49+)
await page.routeWebSocket('**/ws/notifications', (ws) => {
ws.onMessage((msg) => { ws.send(JSON.stringify({ type: 'alert', title: 'Deployed' })); });
});See for HAR replay, conditional routing, and full patterns.
references/network-and-mocking.mdtypescript
// 模拟响应
await page.route('**/api/products*', async (route) => {
await route.fulfill({ json: { items: [{ id: '1', name: 'Widget', price: 29.99 }] } });
});
// 修改真实响应
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 });
});
// 模拟错误
await page.route('**/api/products*', (route) => route.fulfill({ status: 500 }));
// WebSocket (v1.49+)
await page.routeWebSocket('**/ws/notifications', (ws) => {
ws.onMessage((msg) => { ws.send(JSON.stringify({ type: 'alert', title: 'Deployed' })); });
});详见中的HAR重放、条件路由和完整模式。
references/network-and-mocking.mdTags and Annotations
标签与注解
typescript
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 }) => { /* ... */ });typescript
test('结账 @smoke', async ({ page }) => { /* npx playwright test --grep @smoke */ });
test.slow(); // 超时时间变为三倍
test.skip(({ browserName }) => browserName === 'webkit', 'WebKit bug');
test.fixme('已知问题,追踪于JIRA-1234', async ({ page }) => { /* ... */ });Assertions
断言
Web-First Assertions (auto-retry — always prefer these)
Web优先断言(自动重试——优先使用)
typescript
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']);typescript
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']);Soft Assertions
软断言
Collect all failures instead of stopping at the first; all are reported at the end.
typescript
await expect.soft(page.getByLabel('Name')).toHaveValue('Jane Doe');
await expect.soft(page.getByLabel('Email')).toHaveValue('jane@example.com');收集所有失败,而非在第一个失败时停止;所有失败会在最后统一报告。
typescript
await expect.soft(page.getByLabel('Name')).toHaveValue('Jane Doe');
await expect.soft(page.getByLabel('Email')).toHaveValue('jane@example.com');ARIA Snapshots
ARIA快照
Verify accessibility tree structure; catches semantic regressions.
typescript
await expect(page.getByRole('navigation', { name: 'Main' })).toMatchAriaSnapshot(`
- navigation "Main":
- link "Home"
- link "Products"
`);验证可访问性树结构;捕获语义回归。
typescript
await expect(page.getByRole('navigation', { name: 'Main' })).toMatchAriaSnapshot(`
- navigation "Main":
- link "Home"
- link "Products"
`);New Features (2025-2026)
新特性(2025-2026)
Agents should be aware of these recent Playwright additions:
| Version | Feature | What it does |
|---|---|---|
| v1.45 | Clock API | |
| v1.45 | | CI flag that fails the run if any test required a retry to pass |
| v1.46 | | Run only tests affected by changed files (git-diff-aware) |
| v1.46 | Component testing | Mock Next.js/SvelteKit/etc. router in component tests |
| v1.46 | ARIA snapshots | |
| v1.49 | | First-class WebSocket interception (replaces CDP hacks) |
| v1.51 | | Per-block timeout/soft configuration |
| v1.57 | Speedboard in HTML reporter | Performance timeline visualization in the built-in report |
| v1.57 | | Wait for a specific stdout pattern instead of just a URL |
Agent需了解这些Playwright新增特性:
| 版本 | 特性 | 功能说明 |
|---|---|---|
| v1.45 | Clock API | |
| v1.45 | | CI标志,若任何测试需要重试才能通过,则运行失败 |
| v1.46 | | 仅运行受文件变更影响的测试(感知git-diff) |
| v1.46 | 组件测试 | 在组件测试中模拟Next.js/SvelteKit等的路由 |
| v1.46 | ARIA快照 | |
| v1.49 | | 一等WebSocket拦截(替代CDP技巧) |
| v1.51 | | 按块配置超时/软断言 |
| v1.57 | HTML报告中的Speedboard | 内置报告中的性能时间线可视化 |
| v1.57 | | 等待特定标准输出模式,而非仅等待URL |
Parallel Execution & CI
并行执行与CI
Worker Configuration
工作器配置
typescript
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? '50%' : undefined,
});typescript
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? '50%' : undefined,
});Sharding Across CI Nodes
CI节点间分片
yaml
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4yaml
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4Multiple Reporters
多报告器
typescript
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'results.json' }],
['github'],
['junit', { outputFile: 'junit.xml' }],
],See for complete GitHub Actions workflows, artifact upload patterns, and sharding with merge.
references/ci-recipes.mdtypescript
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'results.json' }],
['github'],
['junit', { outputFile: 'junit.xml' }],
],详见中的完整GitHub Actions工作流、产物上传模式和分片合并方法。
references/ci-recipes.mdDebugging
调试
- Trace viewer: -- timeline of actions, network, DOM snapshots, console logs.
npx playwright show-trace test-results/my-test/trace.zip - UI mode: -- live browser, step-by-step, time-travel debugging.
npx playwright test --ui - Debug flag: -- headed browser, pauses at each action.
npx playwright test my-test.spec.ts --debug - VS Code extension: -- run/debug from gutter icons, pick locators, watch mode.
ms-playwright.playwright - page.pause(): Opens the Playwright Inspector mid-test. For local debugging only. Never commit to CI code paths.
See for flaky test triage workflows and artifact analysis.
references/debugging-and-triage.md- 追踪查看器:—— 操作、网络、DOM快照、控制台日志的时间线。
npx playwright show-trace test-results/my-test/trace.zip - UI模式:—— 实时浏览器、分步执行、时间旅行调试。
npx playwright test --ui - 调试标志:—— 带界面的浏览器,在每个操作处暂停。
npx playwright test my-test.spec.ts --debug - VS Code扩展:—— 从 gutter 图标运行/调试、选择定位器、监听模式。
ms-playwright.playwright - page.pause():在测试中途打开Playwright检查器。仅用于本地调试。绝不要提交到CI代码路径。
详见中的不稳定测试分类工作流和产物分析方法。
references/debugging-and-triage.mdDone When
完成标准
- exists with
playwright.config.tsdefined for at least one target browser (Chromium minimum; Firefox and WebKit added for CI)projects - Page Object Model files live in the designated directory (or equivalent) with component objects composed via root
e2e/pages/Locator - All locators in test code use ,
getByRole, orgetByLabel— no raw CSS selectors or XPathgetByTestId - CI workflow runs Playwright with across matrix jobs and uploads the HTML report as an artifact on failure
--shard - No calls exist anywhere in test code (
waitForTimeoutorgrep-style lint catches any regressions)forbidOnly
- 存在,且至少为一个目标浏览器(最低要求Chromium;CI中添加Firefox和WebKit)定义了
playwright.config.tsprojects - 页面对象模型文件位于指定目录(或等效目录),组件对象通过根
e2e/pages/组合Locator - 测试代码中的所有定位器均使用、
getByRole或getByLabel—— 无原始CSS选择器或XPathgetByTestId - CI工作流通过在矩阵任务中运行Playwright,并在失败时上传HTML报告作为产物
--shard - 测试代码中不存在任何调用(
waitForTimeout或grep风格的检查可捕获任何回归)forbidOnly
Related Skills and References
相关技能与参考
Reference Files (in references/
)
references/参考文件(位于references/
)
references/| File | Purpose |
|---|---|
| BAD vs GOOD code pairs for every common mistake |
| Auth fixtures, data fixtures, multi-env projects, composition |
| Locator decision tree, getByRole examples, stability scoring |
| storageState, multi-role, token seeding, session expiry |
| Shared fixtures, per-site config, monorepo patterns |
| page.route, route.fetch, HAR, WebSocket, conditional routing |
| Trace viewer, flaky test triage, retries, artifacts |
| Reporters, sharding, --only-changed, browser caching, artifacts |
| 文件 | 用途 |
|---|---|
| 每个常见错误的错误/正确代码对比 |
| 认证fixtures、数据fixtures、多环境项目、组合 |
| 定位器决策树、getByRole示例、稳定性评分 |
| storageState、多角色、令牌注入、会话过期 |
| 共享fixtures、按站点配置、单体仓库模式 |
| page.route、route.fetch、HAR、WebSocket、条件路由 |
| 追踪查看器、不稳定测试分类、重试、产物 |
| 报告器、分片、--only-changed、浏览器缓存、产物 |
Related Skills
相关技能
- visual-testing -- Screenshot comparison, threshold management, baseline workflows.
- ci-cd-integration -- Pipeline configuration, parallelization, reporting beyond Playwright.
- api-testing -- Backend API validation, contract testing, request/response schemas.
- test-reliability -- Flaky test patterns, retry strategies, test stability metrics.
- accessibility-testing -- WCAG compliance, axe-core integration, ARIA assertions.
- visual-testing -- 截图对比、阈值管理、基准工作流。
- ci-cd-integration -- 流水线配置、并行化、Playwright之外的报告。
- api-testing -- 后端API验证、契约测试、请求/响应 Schema。
- test-reliability -- 不稳定测试模式、重试策略、测试稳定性指标。
- accessibility-testing -- WCAG合规、axe-core集成、ARIA断言。