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:
  1. TypeScript or JavaScript? TypeScript is strongly recommended. It catches locator and assertion mistakes at compile time, and every example in this skill assumes TypeScript.
  2. Which browsers? Chromium for local dev. Add Firefox and WebKit in CI. Mobile viewports are separate Playwright projects, not separate test files.
  3. 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.
  4. Single site or multi-site? Multi-site architectures need shared fixtures and per-site config objects. See
    references/multi-site-architecture.md
    .

在生成任何代码之前,请先询问:
  1. 使用TypeScript还是JavaScript? 强烈推荐TypeScript。它能在编译阶段捕获定位器和断言错误,本技能中的所有示例均默认使用TypeScript。
  2. 测试哪些浏览器? 本地开发使用Chromium。CI环境中添加Firefox和WebKit。移动端视口作为独立的Playwright项目,而非单独的测试文件。
  3. 已有测试套件还是从零开始? 如果从Cypress或Selenium迁移,优先重写最不稳定的测试。切勿尝试一次性全部重写。
  4. 单站点还是多站点架构? 多站点架构需要共享fixtures和每个站点的配置对象。详见
    references/multi-site-architecture.md

Core Principles

核心原则

  1. User-facing locators first.
    getByRole
    >
    getByLabel
    >
    getByTestId
    > CSS (last resort). Locators must reflect what the user sees, not how the DOM is structured. See
    references/selector-strategies.md
    .
  2. Auto-waiting -- NEVER use
    waitForTimeout
    .
    Every Playwright action and web-first assertion auto-waits. If you think you need a timeout, you need a better locator or assertion.
  3. Test isolation. Each test gets a fresh
    BrowserContext
    . Tests must never depend on other tests' state or execution order.
  4. Parallel by default, serial only when necessary. Use
    fullyParallel: true
    in config. Reserve
    test.describe.serial
    for flows that genuinely cannot be isolated (rare).
  5. Fixtures for setup, not hooks. Fixtures compose, provide type safety, and automatically tear down. Prefer them over
    beforeEach
    /
    afterEach
    for anything non-trivial. See
    references/fixtures-and-projects.md
    .
Calibrate to your team maturity (set
team_maturity
in
.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.

  1. 优先使用面向用户的定位器
    getByRole
    >
    getByLabel
    >
    getByTestId
    > CSS(最后选择)。定位器必须反映用户看到的内容,而非DOM的结构。详见
    references/selector-strategies.md
  2. 自动等待——绝不使用
    waitForTimeout
    。Playwright的每个操作和Web优先断言都会自动等待。如果你认为需要超时,说明你需要更好的定位器或断言。
  3. 测试隔离。每个测试都会获得全新的
    BrowserContext
    。测试绝不能依赖其他测试的状态或执行顺序。
  4. 默认并行执行,仅在必要时串行。在配置中设置
    fullyParallel: true
    。仅对确实无法隔离的流程(极少情况)使用
    test.describe.serial
  5. 用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

1. 绝不要用
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

2. 当
getByRole
/
getByLabel
/
getByTestId
可用时,绝不要默认使用CSS/XPath

Why 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.md
中的完整决策树。

3. Never use discouraged
page.*
APIs when locator APIs exist

3. 当定位器API存在时,绝不要使用不推荐的
page.*
API

Why it is wrong:
page.click()
,
page.fill()
,
page.type()
are legacy convenience methods that bypass the locator auto-waiting pipeline and cannot be chained or filtered.
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

4. 绝不要无文档说明地使用
force: true

Why it is wrong:
force: true
skips actionability checks (visible, enabled, stable, receives events). Either the wrong element is targeted, or there is an accessibility bug.
typescript
// 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: true
会跳过可操作性检查(可见、启用、稳定、可接收事件)。要么是定位了错误元素,要么存在可访问性缺陷。
typescript
// 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.md

6. Never put login boilerplate in every test -- use
storageState

6. 绝不要在每个测试中重复登录代码——使用
storageState

Why it is wrong: UI login for every test is slow and fragile.
storageState
logs in once and replays cookies/localStorage for all tests. See
references/auth-patterns.md
.
错误原因:每个测试都通过UI登录会很慢且不稳定。
storageState
只需登录一次,然后为所有测试重放cookie/localStorage。详见
references/auth-patterns.md

7. Never use
locator.all()
on dynamic collections without a stability check

7. 绝不要在动态集合上使用
locator.all()
而不做稳定性检查

Why it is wrong:
locator.all()
returns a snapshot. If the DOM is still updating, you get a partial or empty array. It does not auto-retry.
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
错误原因
locator.all()
返回快照。如果DOM仍在更新,你会得到部分或空数组。它不会自动重试。
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

8. 当
toHaveText()
可提供重试功能时,绝不要用
allTextContents()
做断言

Why it is wrong:
allTextContents()
is a snapshot that does not retry.
toHaveText()
retries until the condition is met or timeout expires.
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

10. 绝不要在提交的代码中保留
test.only

Why it is wrong: A single
test.only
silently skips every other test in the suite. In CI, you run one test and think everything passes.
typescript
export default defineConfig({ forbidOnly: !!process.env.CI });

错误原因:单个
test.only
会静默跳过套件中的所有其他测试。在CI中,你只运行了一个测试,却以为所有测试都通过了。
typescript
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 artifacts
project-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
Locator
, not a
Page
.
typescript
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片段(模态框、数据表、导航栏)。它们接收根
Locator
,而非
Page
typescript
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
storageState
. All test projects load it via config. For multi-role auth (admin/user/guest), see
references/auth-patterns.md
.
全局初始化登录一次并保存
storageState
。所有测试项目通过配置加载它。如需多角色认证(管理员/普通用户/访客),详见
references/auth-patterns.md

Form Interactions with test.step

用test.step处理表单交互

Wrap logical action groups in
test.step()
for better trace viewer output.
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
references/network-and-mocking.md
for HAR replay, conditional routing, and full patterns.
typescript
// 模拟响应
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' })); });
});
详见
references/network-and-mocking.md
中的HAR重放、条件路由和完整模式。

Tags 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:
VersionFeatureWhat it does
v1.45Clock API
page.clock.install()
/
page.clock.fastForward()
-- control time without monkey-patching
Date
v1.45
--fail-on-flaky-tests
CI flag that fails the run if any test required a retry to pass
v1.46
--only-changed
Run only tests affected by changed files (git-diff-aware)
v1.46Component testing
router
fixture
Mock Next.js/SvelteKit/etc. router in component tests
v1.46ARIA snapshots
toMatchAriaSnapshot()
for accessibility tree assertions
v1.49
routeWebSocket
First-class WebSocket interception (replaces CDP hacks)
v1.51
expect.configure
Per-block timeout/soft configuration
v1.57Speedboard in HTML reporterPerformance timeline visualization in the built-in report
v1.57
webServer.wait
regex
Wait for a specific stdout pattern instead of just a URL

Agent需了解这些Playwright新增特性:
版本特性功能说明
v1.45Clock API
page.clock.install()
/
page.clock.fastForward()
—— 无需猴子补丁
Date
即可控制时间
v1.45
--fail-on-flaky-tests
CI标志,若任何测试需要重试才能通过,则运行失败
v1.46
--only-changed
仅运行受文件变更影响的测试(感知git-diff)
v1.46组件测试
router
fixture
在组件测试中模拟Next.js/SvelteKit等的路由
v1.46ARIA快照
toMatchAriaSnapshot()
用于可访问性树断言
v1.49
routeWebSocket
一等WebSocket拦截(替代CDP技巧)
v1.51
expect.configure
按块配置超时/软断言
v1.57HTML报告中的Speedboard内置报告中的性能时间线可视化
v1.57
webServer.wait
正则表达式
等待特定标准输出模式,而非仅等待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 }}/4
yaml
strategy:
  fail-fast: false
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}/4

Multiple Reporters

多报告器

typescript
reporter: [
  ['html', { open: 'never' }],
  ['json', { outputFile: 'results.json' }],
  ['github'],
  ['junit', { outputFile: 'junit.xml' }],
],
See
references/ci-recipes.md
for complete GitHub Actions workflows, artifact upload patterns, and sharding with merge.

typescript
reporter: [
  ['html', { open: 'never' }],
  ['json', { outputFile: 'results.json' }],
  ['github'],
  ['junit', { outputFile: 'junit.xml' }],
],
详见
references/ci-recipes.md
中的完整GitHub Actions工作流、产物上传模式和分片合并方法。

Debugging

调试

  • Trace viewer:
    npx playwright show-trace test-results/my-test/trace.zip
    -- timeline of actions, network, DOM snapshots, console logs.
  • UI mode:
    npx playwright test --ui
    -- live browser, step-by-step, time-travel debugging.
  • Debug flag:
    npx playwright test my-test.spec.ts --debug
    -- headed browser, pauses at each action.
  • VS Code extension:
    ms-playwright.playwright
    -- run/debug from gutter icons, pick locators, watch mode.
  • page.pause(): Opens the Playwright Inspector mid-test. For local debugging only. Never commit to CI code paths.
See
references/debugging-and-triage.md
for flaky test triage workflows and artifact analysis.

  • 追踪查看器
    npx playwright show-trace test-results/my-test/trace.zip
    —— 操作、网络、DOM快照、控制台日志的时间线。
  • UI模式
    npx playwright test --ui
    —— 实时浏览器、分步执行、时间旅行调试。
  • 调试标志
    npx playwright test my-test.spec.ts --debug
    —— 带界面的浏览器,在每个操作处暂停。
  • VS Code扩展
    ms-playwright.playwright
    —— 从 gutter 图标运行/调试、选择定位器、监听模式。
  • page.pause():在测试中途打开Playwright检查器。仅用于本地调试。绝不要提交到CI代码路径。
详见
references/debugging-and-triage.md
中的不稳定测试分类工作流和产物分析方法。

Done When

完成标准

  • playwright.config.ts
    exists with
    projects
    defined for at least one target browser (Chromium minimum; Firefox and WebKit added for CI)
  • Page Object Model files live in the designated directory (
    e2e/pages/
    or equivalent) with component objects composed via root
    Locator
  • All locators in test code use
    getByRole
    ,
    getByLabel
    , or
    getByTestId
    — no raw CSS selectors or XPath
  • CI workflow runs Playwright with
    --shard
    across matrix jobs and uploads the HTML report as an artifact on failure
  • No
    waitForTimeout
    calls exist anywhere in test code (
    grep
    or
    forbidOnly
    -style lint catches any regressions)
  • 存在
    playwright.config.ts
    ,且至少为一个目标浏览器(最低要求Chromium;CI中添加Firefox和WebKit)定义了
    projects
  • 页面对象模型文件位于指定目录(
    e2e/pages/
    或等效目录),组件对象通过根
    Locator
    组合
  • 测试代码中的所有定位器均使用
    getByRole
    getByLabel
    getByTestId
    —— 无原始CSS选择器或XPath
  • CI工作流通过
    --shard
    在矩阵任务中运行Playwright,并在失败时上传HTML报告作为产物
  • 测试代码中不存在任何
    waitForTimeout
    调用(
    grep
    forbidOnly
    风格的检查可捕获任何回归)

Related Skills and References

相关技能与参考

Reference Files (in
references/
)

参考文件(位于
references/

FilePurpose
anti-patterns.md
BAD vs GOOD code pairs for every common mistake
fixtures-and-projects.md
Auth fixtures, data fixtures, multi-env projects, composition
selector-strategies.md
Locator decision tree, getByRole examples, stability scoring
auth-patterns.md
storageState, multi-role, token seeding, session expiry
multi-site-architecture.md
Shared fixtures, per-site config, monorepo patterns
network-and-mocking.md
page.route, route.fetch, HAR, WebSocket, conditional routing
debugging-and-triage.md
Trace viewer, flaky test triage, retries, artifacts
ci-recipes.md
Reporters, sharding, --only-changed, browser caching, artifacts
文件用途
anti-patterns.md
每个常见错误的错误/正确代码对比
fixtures-and-projects.md
认证fixtures、数据fixtures、多环境项目、组合
selector-strategies.md
定位器决策树、getByRole示例、稳定性评分
auth-patterns.md
storageState、多角色、令牌注入、会话过期
multi-site-architecture.md
共享fixtures、按站点配置、单体仓库模式
network-and-mocking.md
page.route、route.fetch、HAR、WebSocket、条件路由
debugging-and-triage.md
追踪查看器、不稳定测试分类、重试、产物
ci-recipes.md
报告器、分片、--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断言。