playwright-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright Best Practices
Playwright 最佳实践
CLI Context: Prevent Context Overflow
CLI 上下文:防止上下文溢出
When running Playwright tests from Claude Code or any CLI agent, always use minimal reporters to prevent verbose output from consuming the context window.
Use or for CLI test runs:
--reporter=line--reporter=dotbash
undefined从Claude Code或任何CLI Agent运行Playwright测试时,请始终使用极简报告器,以避免冗长输出占用上下文窗口。
CLI测试运行时使用或:
--reporter=line--reporter=dotbash
undefinedREQUIRED: Use minimal reporter to prevent context overflow
必须:使用极简报告器防止上下文溢出
npx playwright test --reporter=line
npx playwright test --reporter=dot
npx playwright test --reporter=line
npx playwright test --reporter=dot
BAD: Default reporter generates thousands of lines, floods context
不推荐:默认报告器会生成数千行内容,占用上下文
npx playwright test
Configure `playwright.config.ts` to use minimal reporters by default when `CI` or `CLAUDE` env vars are set:
```ts
reporter: process.env.CI || process.env.CLAUDE
? [['line'], ['html', { open: 'never' }]]
: 'list',npx playwright test
配置`playwright.config.ts`,当设置`CI`或`CLAUDE`环境变量时,默认使用极简报告器:
```ts
reporter: process.env.CI || process.env.CLAUDE
? [['line'], ['html', { open: 'never' }]]
: 'list',Locator Priority (Most to Least Resilient)
定位器优先级(从最可靠到最不可靠)
Always prefer user-facing attributes:
- - accessibility roles
page.getByRole('button', { name: 'Submit' }) - - form control labels
page.getByLabel('Email') - - input placeholders
page.getByPlaceholder('Search...') - - visible text (non-interactive)
page.getByText('Welcome') - - image alt text
page.getByAltText('Logo') - - title attributes
page.getByTitle('Settings') - - explicit test contracts
page.getByTestId('submit-btn') - CSS/XPath - last resort, avoid
ts
// BAD: Brittle selectors tied to implementation
page.locator('button.btn-primary.submit-form')
page.locator('//div[@class="container"]/form/button')
page.locator('#app > div:nth-child(2) > button')
// GOOD: User-facing, resilient locators
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Password')始终优先使用面向用户的属性:
- - 可访问性角色
page.getByRole('button', { name: 'Submit' }) - - 表单控件标签
page.getByLabel('Email') - - 输入框占位符
page.getByPlaceholder('Search...') - - 可见文本(非交互元素)
page.getByText('Welcome') - - 图片替代文本
page.getByAltText('Logo') - - 标题属性
page.getByTitle('Settings') - - 显式测试契约
page.getByTestId('submit-btn') - CSS/XPath - 最后手段,尽量避免
ts
// 不推荐:与实现绑定的脆弱选择器
page.locator('button.btn-primary.submit-form')
page.locator('//div[@class="container"]/form/button')
page.locator('#app > div:nth-child(2) > button')
// 推荐:面向用户的可靠定位器
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Password')Chaining and Filtering
链式调用与过滤
ts
// Scope within a region
const card = page.getByRole('listitem').filter({ hasText: 'Product A' });
await card.getByRole('button', { name: 'Add to cart' }).click();
// Filter by child locator
const row = page.getByRole('row').filter({
has: page.getByRole('cell', { name: 'John' })
});
// Combine conditions
const visibleSubmit = page.getByRole('button', { name: 'Submit' }).and(page.locator(':visible'));
const primaryOrSecondary = page.getByRole('button', { name: 'Save' }).or(page.getByRole('button', { name: 'Update' }));ts
// 在指定区域内定位
const card = page.getByRole('listitem').filter({ hasText: 'Product A' });
await card.getByRole('button', { name: 'Add to cart' }).click();
// 通过子定位器过滤
const row = page.getByRole('row').filter({
has: page.getByRole('cell', { name: 'John' })
});
// 组合条件
const visibleSubmit = page.getByRole('button', { name: 'Submit' }).and(page.locator(':visible'));
const primaryOrSecondary = page.getByRole('button', { name: 'Save' }).or(page.getByRole('button', { name: 'Update' }));Strictness
严格性
Locators throw if multiple elements match. Use , , only when intentional:
first()last()nth()ts
// Throws if multiple buttons match
await page.getByRole('button', { name: 'Delete' }).click();
// Explicit selection when needed
await page.getByRole('listitem').first().click();
await page.getByRole('row').nth(2).getByRole('button').click();如果多个元素匹配,定位器会抛出错误。仅在必要时使用、、:
first()last()nth()ts
// 如果多个按钮匹配,会抛出错误
await page.getByRole('button', { name: 'Delete' }).click();
// 必要时显式选择
await page.getByRole('listitem').first().click();
await page.getByRole('row').nth(2).getByRole('button').click();Web-First Assertions
Web优先断言
Use async assertions that auto-wait and retry:
ts
// BAD: No auto-wait, flaky
expect(await page.getByText('Success').isVisible()).toBe(true);
// GOOD: Auto-waits up to timeout
await expect(page.getByText('Success')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByTestId('status')).toHaveText('Submitted');
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('Dashboard');
// Collections
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('listitem')).toHaveText(['Item 1', 'Item 2', 'Item 3']);
// Soft assertions (continue on failure, report all)
await expect.soft(locator).toBeVisible();
await expect.soft(locator).toHaveText('Expected');
// Test continues, failures compiled at end使用支持自动等待和重试的异步断言:
ts
// 不推荐:无自动等待,不稳定
expect(await page.getByText('Success').isVisible()).toBe(true);
// 推荐:自动等待直至超时
await expect(page.getByText('Success')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByTestId('status')).toHaveText('Submitted');
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('Dashboard');
// 集合断言
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('listitem')).toHaveText(['Item 1', 'Item 2', 'Item 3']);
// 软断言(失败后继续执行,最终汇总所有失败)
await expect.soft(locator).toBeVisible();
await expect.soft(locator).toHaveText('Expected');
// 测试继续执行,失败信息在末尾汇总Page Object Model
Page Object Model
Encapsulate page interactions. Define locators as readonly properties in constructor.
ts
// pages/base.page.ts
import { type Page, type Locator, expect } from '@playwright/test';
import debug from 'debug';
export abstract class BasePage {
protected readonly log: debug.Debugger;
constructor(
protected readonly page: Page,
protected readonly timeout = 30_000
) {
this.log = debug(`test:page:${this.constructor.name}`);
}
protected async safeClick(locator: Locator, description?: string) {
this.log('clicking: %s', description ?? locator);
await expect(locator).toBeVisible({ timeout: this.timeout });
await expect(locator).toBeEnabled({ timeout: this.timeout });
await locator.click();
}
protected async safeFill(locator: Locator, value: string) {
await expect(locator).toBeVisible({ timeout: this.timeout });
await locator.fill(value);
}
abstract isLoaded(): Promise<void>;
}ts
// pages/login.page.ts
import { type Locator, type Page, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
await this.isLoaded();
}
async isLoaded() {
await expect(this.emailInput).toBeVisible();
}
async login(email: string, password: string) {
await this.safeFill(this.emailInput, email);
await this.safeFill(this.passwordInput, password);
await this.safeClick(this.submitButton, 'Sign in button');
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}封装页面交互。在构造函数中将定位器定义为只读属性。
ts
// pages/base.page.ts
import { type Page, type Locator, expect } from '@playwright/test';
import debug from 'debug';
export abstract class BasePage {
protected readonly log: debug.Debugger;
constructor(
protected readonly page: Page,
protected readonly timeout = 30_000
) {
this.log = debug(`test:page:${this.constructor.name}`);
}
protected async safeClick(locator: Locator, description?: string) {
this.log('clicking: %s', description ?? locator);
await expect(locator).toBeVisible({ timeout: this.timeout });
await expect(locator).toBeEnabled({ timeout: this.timeout });
await locator.click();
}
protected async safeFill(locator: Locator, value: string) {
await expect(locator).toBeVisible({ timeout: this.timeout });
await locator.fill(value);
}
abstract isLoaded(): Promise<void>;
}ts
// pages/login.page.ts
import { type Locator, type Page, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
await this.isLoaded();
}
async isLoaded() {
await expect(this.emailInput).toBeVisible();
}
async login(email: string, password: string) {
await this.safeFill(this.emailInput, email);
await this.safeFill(this.passwordInput, password);
await this.safeClick(this.submitButton, 'Sign in button');
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}Fixtures
Fixture
Prefer fixtures over beforeEach/afterEach. Fixtures encapsulate setup + teardown, run on-demand, and compose with dependencies.
ts
// fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
type TestFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<TestFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect };优先使用Fixture而非beforeEach/afterEach。Fixture封装了初始化+清理逻辑,按需运行,并可与依赖项组合。
ts
// fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
type TestFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<TestFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect };Worker-Scoped Fixtures
Worker作用域Fixture
Use for expensive setup shared across tests (database connections, authenticated users):
ts
// fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
type WorkerFixtures = {
authenticatedUser: { token: string; userId: string };
};
export const test = base.extend<{}, WorkerFixtures>({
authenticatedUser: [async ({}, use) => {
// Expensive setup - runs once per worker
const user = await createTestUser();
const token = await authenticateUser(user);
await use({ token, userId: user.id });
// Cleanup after all tests in worker
await deleteTestUser(user.id);
}, { scope: 'worker' }],
});用于跨测试共享的昂贵初始化操作(数据库连接、已认证用户):
ts
// fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
type WorkerFixtures = {
authenticatedUser: { token: string; userId: string };
};
export const test = base.extend<{}, WorkerFixtures>({
authenticatedUser: [async ({}, use) => {
// 昂贵的初始化 - 每个Worker运行一次
const user = await createTestUser();
const token = await authenticateUser(user);
await use({ token, userId: user.id });
// Worker中所有测试完成后清理
await deleteTestUser(user.id);
}, { scope: 'worker' }],
});Automatic Fixtures
自动Fixture
Run for every test without explicit declaration:
ts
export const test = base.extend<{ autoLog: void }>({
autoLog: [async ({ page }, use) => {
page.on('console', msg => console.log(`[browser] ${msg.text()}`));
await use();
}, { auto: true }],
});无需显式声明即可为每个测试运行:
ts
export const test = base.extend<{ autoLog: void }>({
autoLog: [async ({ page }, use) => {
page.on('console', msg => console.log(`[browser] ${msg.text()}`));
await use();
}, { auto: true }],
});Authentication
认证
Save authenticated state to reuse. Never log in via UI in every test.
ts
// auth.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(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});ts
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});保存认证状态以复用。切勿在每个测试中都通过UI登录。
ts
// auth.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(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});ts
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});API Authentication (Faster)
API认证(更快)
ts
setup('authenticate via API', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: process.env.TEST_USER_EMAIL, password: process.env.TEST_USER_PASSWORD },
});
expect(response.ok()).toBeTruthy();
await request.storageState({ path: authFile });
});ts
setup('authenticate via API', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: process.env.TEST_USER_EMAIL, password: process.env.TEST_USER_PASSWORD },
});
expect(response.ok()).toBeTruthy();
await request.storageState({ path: authFile });
});Network Mocking
网络模拟
Set up routes before navigation.
ts
test('displays mocked data', async ({ page }) => {
await page.route('**/api/users', route => route.fulfill({
json: [{ id: 1, name: 'Test User' }],
}));
await page.goto('/users');
await expect(page.getByText('Test User')).toBeVisible();
});
// Modify real response
test('injects item into response', async ({ page }) => {
await page.route('**/api/items', async route => {
const response = await route.fetch();
const json = await response.json();
json.push({ id: 999, name: 'Injected' });
await route.fulfill({ response, json });
});
await page.goto('/items');
});
// HAR recording
test('uses recorded responses', async ({ page }) => {
await page.routeFromHAR('./fixtures/api.har', {
url: '**/api/**',
update: false, // true to record
});
await page.goto('/');
});在导航前设置路由。
ts
test('displays mocked data', async ({ page }) => {
await page.route('**/api/users', route => route.fulfill({
json: [{ id: 1, name: 'Test User' }],
}));
await page.goto('/users');
await expect(page.getByText('Test User')).toBeVisible();
});
// 修改真实响应
test('injects item into response', async ({ page }) => {
await page.route('**/api/items', async route => {
const response = await route.fetch();
const json = await response.json();
json.push({ id: 999, name: 'Injected' });
await route.fulfill({ response, json });
});
await page.goto('/items');
});
// HAR录制
test('uses recorded responses', async ({ page }) => {
await page.routeFromHAR('./fixtures/api.har', {
url: '**/api/**',
update: false, // true表示录制
});
await page.goto('/');
});Test Isolation
测试隔离
Each test gets fresh browser context. Never share state between tests.
ts
// BAD: Tests depend on each other
let userId: string;
test('create user', async ({ request }) => {
userId = (await (await request.post('/api/users', { data: { name: 'Test' } })).json()).id;
});
test('delete user', async ({ request }) => {
await request.delete(`/api/users/${userId}`); // Depends on previous!
});
// GOOD: Each test creates its own data
test('can delete created user', async ({ request }) => {
const { id } = await (await request.post('/api/users', { data: { name: 'Test' } })).json();
const deleteResponse = await request.delete(`/api/users/${id}`);
expect(deleteResponse.ok()).toBeTruthy();
});每个测试都获得全新的浏览器上下文。切勿在测试之间共享状态。
ts
// 不推荐:测试之间相互依赖
let userId: string;
test('create user', async ({ request }) => {
userId = (await (await request.post('/api/users', { data: { name: 'Test' } })).json()).id;
});
test('delete user', async ({ request }) => {
await request.delete(`/api/users/${userId}`); // 依赖前一个测试!
});
// 推荐:每个测试创建自己的测试数据
test('can delete created user', async ({ request }) => {
const { id } = await (await request.post('/api/users', { data: { name: 'Test' } })).json();
const deleteResponse = await request.delete(`/api/users/${id}`);
expect(deleteResponse.ok()).toBeTruthy();
});Configuration
配置
ts
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// Use minimal reporter in CI/agent contexts to prevent context overflow
reporter: process.env.CI || process.env.CLAUDE
? [['line'], ['html', { open: 'never' }]]
: 'list',
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});ts
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// 在CI/Agent环境中使用极简报告器防止上下文溢出
reporter: process.env.CI || process.env.CLAUDE
? [['line'], ['html', { open: 'never' }]]
: 'list',
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});Project Structure
项目结构
tests/
fixtures/ # Custom fixtures (extend base test)
pages/ # Page Object Models
helpers/ # Utility functions (API clients, data factories)
auth.setup.ts # Authentication setup project
*.spec.ts # Test files
playwright/
.auth/ # Auth state storage (gitignored)
playwright.config.tsOrganize tests by feature or user journey. Colocate page objects with tests when possible.
tests/
fixtures/ # 自定义Fixture(扩展基础测试)
pages/ # Page Object Model
helpers/ # 工具函数(API客户端、数据工厂)
auth.setup.ts # 认证初始化项目
*.spec.ts # 测试文件
playwright/
.auth/ # 认证状态存储(已加入git忽略)
playwright.config.ts按功能或用户旅程组织测试。尽可能将Page Object与测试放在同一目录下。
Helpers (Separate from Pages)
工具函数(与页面分离)
ts
// helpers/user.helper.ts
import type { Page } from '@playwright/test';
import debug from 'debug';
const log = debug('test:helper:user');
export class UserHelper {
constructor(private page: Page) {}
async createUser(data: { name: string; email: string }) {
log('creating user: %s', data.email);
const response = await this.page.request.post('/api/users', { data });
return response.json();
}
async deleteUser(id: string) {
log('deleting user: %s', id);
await this.page.request.delete(`/api/users/${id}`);
}
}
// helpers/data.factory.ts
export function createTestUser(overrides: Partial<User> = {}): User {
return {
id: crypto.randomUUID(),
email: `test-${Date.now()}@example.com`,
name: 'Test User',
...overrides,
};
}ts
// helpers/user.helper.ts
import type { Page } from '@playwright/test';
import debug from 'debug';
const log = debug('test:helper:user');
export class UserHelper {
constructor(private page: Page) {}
async createUser(data: { name: string; email: string }) {
log('creating user: %s', data.email);
const response = await this.page.request.post('/api/users', { data });
return response.json();
}
async deleteUser(id: string) {
log('deleting user: %s', id);
await this.page.request.delete(`/api/users/${id}`);
}
}
// helpers/data.factory.ts
export function createTestUser(overrides: Partial<User> = {}): User {
return {
id: crypto.randomUUID(),
email: `test-${Date.now()}@example.com`,
name: 'Test User',
...overrides,
};
}Debugging
调试
bash
npx playwright test --debug # Step through with inspector
npx playwright test --trace on # Record trace for all tests
npx playwright test --ui # Interactive UI mode
npx playwright codegen localhost:3000 # Generate locators interactively
npx playwright show-report # View HTML reportEnable debug logs:
DEBUG=test:* npx playwright testbash
npx playwright test --debug # 使用检查器逐步调试
npx playwright test --trace on # 为所有测试录制追踪信息
npx playwright test --ui # 交互式UI模式
npx playwright codegen localhost:3000 # 交互式生成定位器
npx playwright show-report # 查看HTML报告启用调试日志:
DEBUG=test:* npx playwright testAnti-Patterns
反模式
- - use auto-waiting locators instead
page.waitForTimeout(ms) - - use role/label/testid
page.locator('.class') - XPath selectors - fragile, use user-facing attributes
- Shared state between tests - each test creates own data
- UI login in every test - use setup project + storageState
- Manual assertions without await - use web-first assertions
- Hardcoded waits - rely on Playwright's auto-waiting
- Default reporter in CI/agent - use or
--reporter=lineto prevent context overflow--reporter=dot
- - 改用支持自动等待的定位器
page.waitForTimeout(ms) - - 使用role/label/testid
page.locator('.class') - XPath选择器 - 脆弱,使用面向用户的属性
- 测试之间共享状态 - 每个测试创建自己的数据
- 每个测试都通过UI登录 - 使用初始化项目+storageState
- 不使用await的手动断言 - 使用Web优先断言
- 硬编码等待 - 依赖Playwright的自动等待机制
- 在CI/Agent中使用默认报告器 - 使用或
--reporter=line防止上下文溢出--reporter=dot
Checklist
检查清单
- Locators use role/label/testid, not CSS classes or XPath
- All assertions use web-first matchers
await expect() - Page objects define locators in constructor
- No - use auto-waiting
page.waitForTimeout() - Tests isolated - no shared state
- Auth state reused via setup project
- Network mocks set up before navigation
- Test data created per-test or via fixtures
- Debug logging added for complex flows
- Minimal reporter (/
line) used in CI/agent contextsdot
- 定位器使用role/label/testid,而非CSS类或XPath
- 所有断言使用Web优先匹配器
await expect() - Page Object在构造函数中定义定位器
- 不使用- 改用自动等待
page.waitForTimeout() - 测试隔离 - 无共享状态
- 通过初始化项目复用认证状态
- 网络模拟在导航前设置
- 测试数据按测试创建或通过Fixture提供
- 为复杂流程添加调试日志
- 在CI/Agent环境中使用极简报告器(/
line)dot