playwright-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright Automation Patterns
Playwright自动化模式
Overview
概述
Reliable browser automation requires strategic selector choice, proper waiting, and defensive coding. This skill provides patterns that minimize test flakiness and maximize maintainability.
可靠的浏览器自动化需要合理的选择器选型、正确的等待机制以及防御式编码。本技能提供的模式可最小化测试的不稳定性,同时提升可维护性。
When to Use
适用场景
- Writing new Playwright scripts or tests
- Debugging flaky automation
- Refactoring unreliable selectors
- Building web scrapers that need to handle dynamic content
- Creating E2E tests that must be maintainable
When NOT to use:
- Simple one-time browser tasks
- When you need Playwright API documentation (use context7 MCP)
- 编写新的Playwright脚本或测试用例
- 调试不稳定的自动化代码
- 重构不可靠的选择器
- 构建需要处理动态内容的网页爬虫
- 创建可维护的E2E测试
不适用场景:
- 简单的一次性浏览器任务
- 需要查阅Playwright API文档时(使用context7 MCP)
Selector Strategy
选择器策略
Priority Order
优先级顺序
Use user-facing locators first (most resilient), then test IDs, then CSS/XPath as last resort:
-
Role-based locators (best - user-centric)javascript
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com'); -
Other user-facing locatorsjavascript
await page.getByLabel('Password').fill('secret'); await page.getByPlaceholder('Search...').fill('query'); await page.getByText('Submit Order').click(); -
Test ID attributes (explicit contract)javascript
// Default uses data-testid await page.getByTestId('submit-button').click(); // Can customize in playwright.config.ts: // use: { testIdAttribute: 'data-pw' } -
CSS/ID selectors (fragile, avoid if possible)javascript
await page.locator('#submit-btn').click(); await page.locator('.btn.btn-primary.submit').click();
优先使用面向用户的定位器(最具韧性),其次是测试ID,最后才考虑CSS/XPath:
-
基于角色的定位器(最优 - 以用户为中心)javascript
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com'); -
其他面向用户的定位器javascript
await page.getByLabel('Password').fill('secret'); await page.getByPlaceholder('Search...').fill('query'); await page.getByText('Submit Order').click(); -
测试ID属性(显式约定)javascript
// Default uses data-testid await page.getByTestId('submit-button').click(); // Can customize in playwright.config.ts: // use: { testIdAttribute: 'data-pw' } -
CSS/ID选择器(易失效,尽量避免)javascript
await page.locator('#submit-btn').click(); await page.locator('.btn.btn-primary.submit').click();
Strictness and Specificity
严格性与特异性
Locators are strict by default - operations throw if multiple elements match:
javascript
// ERROR if 2+ buttons exist
await page.getByRole('button').click();
// Solutions:
// 1. Make locator more specific
await page.getByRole('button', { name: 'Submit' }).click();
// 2. Filter to narrow down
await page.getByRole('button')
.filter({ hasText: 'Submit' })
.click();
// 3. Chain locators to scope
await page.locator('.product-card')
.getByRole('button', { name: 'Add to cart' })
.click();
// Avoid: Using first() makes tests fragile
await page.getByRole('button').first().click(); // Don't do this定位器默认是严格的——如果有多个元素匹配,操作会抛出错误:
javascript
// ERROR if 2+ buttons exist
await page.getByRole('button').click();
// Solutions:
// 1. Make locator more specific
await page.getByRole('button', { name: 'Submit' }).click();
// 2. Filter to narrow down
await page.getByRole('button')
.filter({ hasText: 'Submit' })
.click();
// 3. Chain locators to scope
await page.locator('.product-card')
.getByRole('button', { name: 'Add to cart' })
.click();
// Avoid: Using first() makes tests fragile
await page.getByRole('button').first().click(); // Don't do thisLocator Filtering and Chaining
定位器过滤与链式调用
javascript
// Filter by text content
await page.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button')
.click();
// Filter by child element
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Buy' })
.click();
// Filter by NOT having text
await expect(
page.getByRole('listitem')
.filter({ hasNot: page.getByText('Out of stock') })
).toHaveCount(5);
// Handle "either/or" scenarios
const loginOrWelcome = await page.getByRole('button', { name: 'Login' })
.or(page.getByText('Welcome back'))
.first();
await expect(loginOrWelcome).toBeVisible();javascript
// Filter by text content
await page.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button')
.click();
// Filter by child element
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Buy' })
.click();
// Filter by NOT having text
await expect(
page.getByRole('listitem')
.filter({ hasNot: page.getByText('Out of stock') })
).toHaveCount(5);
// Handle "either/or" scenarios
const loginOrWelcome = await page.getByRole('button', { name: 'Login' })
.or(page.getByText('Welcome back'))
.first();
await expect(loginOrWelcome).toBeVisible();Anti-Patterns to Avoid
需避免的反模式
❌ Fragile CSS paths
javascript
// BAD: Breaks when HTML structure changes
await page.click('div.container > div:nth-child(2) > button.submit');✅ Stable semantic selectors
javascript
// GOOD: Survives structural changes
await page.getByRole('button', { name: 'Submit' }).click();❌ XPath with positions
javascript
// BAD: Brittle
await page.locator('xpath=//div[3]/button[1]').click();✅ XPath with content
javascript
// BETTER: More stable
await page.locator('xpath=//button[contains(text(), "Submit")]').click();❌ 易失效的CSS路径
javascript
// BAD: Breaks when HTML structure changes
await page.click('div.container > div:nth-child(2) > button.submit');✅ 稳定的语义化选择器
javascript
// GOOD: Survives structural changes
await page.getByRole('button', { name: 'Submit' }).click();❌ 带位置的XPath
javascript
// BAD: Brittle
await page.locator('xpath=//div[3]/button[1]').click();✅ 带内容的XPath
javascript
// BETTER: More stable
await page.locator('xpath=//button[contains(text(), "Submit")]').click();Waiting Patterns
等待模式
Built-in Auto-Waiting
内置自动等待
Playwright auto-waits before most actions. Trust it.
javascript
// Auto-waits for element to be visible, enabled, and stable
await page.click('button');
await page.fill('input[name="email"]', 'test@example.com');What auto-waiting checks:
- Element is attached to DOM
- Element is visible
- Element is stable (not animating)
- Element is enabled
- Element receives events (not obscured)
javascript
// Bypass checks (use with caution)
await page.click('button', { force: true });
// Test without acting (trial run)
await page.click('button', { trial: true });Playwright会在大多数操作前自动等待,请信任这一机制。
javascript
// Auto-waits for element to be visible, enabled, and stable
await page.click('button');
await page.fill('input[name="email"]', 'test@example.com');自动等待会检查以下条件:
- 元素已附加到DOM
- 元素可见
- 元素稳定(未处于动画状态)
- 元素已启用
- 元素可接收事件(未被遮挡)
javascript
// Bypass checks (use with caution)
await page.click('button', { force: true });
// Test without acting (trial run)
await page.click('button', { trial: true });Web-First Assertions
Web优先断言
Use web-first assertions - they retry until condition is met:
javascript
// WRONG - no retry, immediate check
expect(await page.getByText('welcome').isVisible()).toBe(true);
// CORRECT - auto-retries until timeout
await expect(page.getByText('welcome')).toBeVisible();
await expect(page.getByText('Status')).toHaveText('Complete');
await expect(page.getByRole('listitem')).toHaveCount(5);
// Soft assertions - continue test even on failure
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await page.getByRole('link', { name: 'next' }).click();
// Test continues, failures reported at end使用Web优先断言——它们会重试直到条件满足:
javascript
// WRONG - no retry, immediate check
expect(await page.getByText('welcome').isVisible()).toBe(true);
// CORRECT - auto-retries until timeout
await expect(page.getByText('welcome')).toBeVisible();
await expect(page.getByText('Status')).toHaveText('Complete');
await expect(page.getByRole('listitem')).toHaveCount(5);
// Soft assertions - continue test even on failure
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await page.getByRole('link', { name: 'next' }).click();
// Test continues, failures reported at endExplicit Waits for Dynamic Content
动态内容的显式等待
javascript
// Wait for specific element (modern - use web-first assertions)
await expect(page.locator('.results-loaded')).toBeVisible();
// Wait for network to be idle
await page.waitForLoadState('networkidle');
// Wait for custom condition
await page.waitForFunction(() =>
document.querySelectorAll('.item').length > 10
);javascript
// Wait for specific element (modern - use web-first assertions)
await expect(page.locator('.results-loaded')).toBeVisible();
// Wait for network to be idle
await page.waitForLoadState('networkidle');
// Wait for custom condition
await page.waitForFunction(() =>
document.querySelectorAll('.item').length > 10
);Handling Asynchronous Updates
异步更新处理
javascript
// Known count - assert exact number
await expect(page.locator('.item')).toHaveCount(5);
// Unknown count - wait for container, then extract
await expect(page.locator('.search-results')).toBeVisible();
const items = await page.locator('.item').all();
// Loading spinner - wait for absence then presence
await expect(page.locator('.loading-spinner')).not.toBeVisible();
await expect(page.locator('.results')).toBeVisible();
// Wait for text content to appear
await expect(page.locator('.status')).toHaveText('Complete');
// At least one result (reject zero results)
await expect(page.locator('.item').first()).toBeVisible();javascript
// Known count - assert exact number
await expect(page.locator('.item')).toHaveCount(5);
// Unknown count - wait for container, then extract
await expect(page.locator('.search-results')).toBeVisible();
const items = await page.locator('.item').all();
// Loading spinner - wait for absence then presence
await expect(page.locator('.loading-spinner')).not.toBeVisible();
await expect(page.locator('.results')).toBeVisible();
// Wait for text content to appear
await expect(page.locator('.status')).toHaveText('Complete');
// At least one result (reject zero results)
await expect(page.locator('.item').first()).toBeVisible();Data Extraction Patterns
数据提取模式
Single Element
单个元素
javascript
// textContent() - Gets all text including hidden elements
const title = await page.locator('h1').textContent();
// innerText() - Gets only visible text (respects CSS display)
const price = await page.locator('.price').innerText();
// getAttribute() - Get attribute value
const href = await page.locator('a.product').getAttribute('href');
// For assertions, prefer web-first assertions
await expect(page.locator('.price')).toHaveText('$99');javascript
// textContent() - Gets all text including hidden elements
const title = await page.locator('h1').textContent();
// innerText() - Gets only visible text (respects CSS display)
const price = await page.locator('.price').innerText();
// getAttribute() - Get attribute value
const href = await page.locator('a.product').getAttribute('href');
// For assertions, prefer web-first assertions
await expect(page.locator('.price')).toHaveText('$99');Multiple Elements
多个元素
javascript
// IMPORTANT: locator.all() doesn't wait for elements
// This can be flaky if list is still loading
// Known count - assert first, then extract
await expect(page.locator('.item')).toHaveCount(5);
const items = await page.locator('.item').all();
const data = await Promise.all(
items.map(async item => ({
title: await item.locator('.title').textContent(),
price: await item.locator('.price').textContent(),
}))
);
// Unknown count - wait for container, then extract
await expect(page.locator('.results-container')).toBeVisible();
const data = await page.locator('.item').evaluateAll(items =>
items.map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
}))
);
// BEST: Use evaluateAll for batch extraction (single round-trip)
// Use when: extracting from locator-scoped elements (most common)
const data = await page.locator('.item').evaluateAll(items =>
items.map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
}))
);javascript
// IMPORTANT: locator.all() doesn't wait for elements
// This can be flaky if list is still loading
// Known count - assert first, then extract
await expect(page.locator('.item')).toHaveCount(5);
const items = await page.locator('.item').all();
const data = await Promise.all(
items.map(async item => ({
title: await item.locator('.title').textContent(),
price: await item.locator('.price').textContent(),
}))
);
// Unknown count - wait for container, then extract
await expect(page.locator('.results-container')).toBeVisible();
const data = await page.locator('.item').evaluateAll(items =>
items.map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
}))
);
// BEST: Use evaluateAll for batch extraction (single round-trip)
// Use when: extracting from locator-scoped elements (most common)
const data = await page.locator('.item').evaluateAll(items =>
items.map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
}))
);Complex Extraction with evaluate()
使用evaluate()进行复杂提取
javascript
// Use evaluate() when you need global page context
// (e.g., checking window variables, document state)
const data = await page.evaluate(() => {
return {
items: Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
available: !el.classList.contains('out-of-stock')
})),
totalCount: window.productCount, // Access global variables
filters: window.appliedFilters // Page-level state
};
});
// Prefer evaluateAll() for locator-scoped extraction (more focused)
const items = await page.locator('.item').evaluateAll(els =>
els.map(el => ({ /* ... */ }))
);javascript
// Use evaluate() when you need global page context
// (e.g., checking window variables, document state)
const data = await page.evaluate(() => {
return {
items: Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('.title')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
available: !el.classList.contains('out-of-stock')
})),
totalCount: window.productCount, // Access global variables
filters: window.appliedFilters // Page-level state
};
});
// Prefer evaluateAll() for locator-scoped extraction (more focused)
const items = await page.locator('.item').evaluateAll(els =>
els.map(el => ({ /* ... */ }))
);Error Handling
错误处理
Graceful Fallbacks
优雅降级
javascript
// Check if element exists before interacting
const cookieBanner = page.locator('.cookie-banner');
if (await cookieBanner.isVisible()) {
await cookieBanner.getByRole('button', { name: 'Accept' }).click();
}javascript
// Check if element exists before interacting
const cookieBanner = page.locator('.cookie-banner');
if (await cookieBanner.isVisible()) {
await cookieBanner.getByRole('button', { name: 'Accept' }).click();
}Retry Logic
重试逻辑
javascript
// Playwright retries automatically, but you can customize
await expect(async () => {
const status = await page.locator('.status').textContent();
expect(status).toBe('Complete');
}).toPass({ timeout: 10000, intervals: [1000] });javascript
// Playwright retries automatically, but you can customize
await expect(async () => {
const status = await page.locator('.status').textContent();
expect(status).toBe('Complete');
}).toPass({ timeout: 10000, intervals: [1000] });Timeout Configuration
超时配置
javascript
// Set timeout for specific action
await page.click('button', { timeout: 5000 });
// Set timeout for entire test
test.setTimeout(60000);
// Set default timeout for page
page.setDefaultTimeout(10000);javascript
// Set timeout for specific action
await page.click('button', { timeout: 5000 });
// Set timeout for entire test
test.setTimeout(60000);
// Set default timeout for page
page.setDefaultTimeout(10000);Navigation Patterns
导航模式
Wait for Navigation
等待导航完成
javascript
// Modern pattern - click auto-waits for navigation
await page.click('a.next-page');
await page.waitForLoadState('networkidle'); // Only if needed
// Using modern locator
await page.getByRole('link', { name: 'Next Page' }).click();javascript
// Modern pattern - click auto-waits for navigation
await page.click('a.next-page');
await page.waitForLoadState('networkidle'); // Only if needed
// Using modern locator
await page.getByRole('link', { name: 'Next Page' }).click();Multi-Page Workflows
多页面流程
javascript
// Open new tab
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target="_blank"]')
]);
await newPage.waitForLoadState();
// Work with newPage
await newPage.close();javascript
// Open new tab
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target="_blank"]')
]);
await newPage.waitForLoadState();
// Work with newPage
await newPage.close();Form Interaction Patterns
表单交互模式
Basic Form Filling
基础表单填充
javascript
// fill() - Recommended for most inputs (fast, atomic operation)
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'secret123');
// type() - For keystroke-sensitive inputs (slower, fires each key event)
await page.locator('input.search').type('Product', { delay: 100 });
// Modern approach with role-based locators
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('combobox', { name: 'Country' }).selectOption('US');
await page.getByRole('checkbox', { name: 'I agree' }).check();
await page.getByRole('button', { name: 'Submit' }).click();javascript
// fill() - Recommended for most inputs (fast, atomic operation)
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'secret123');
// type() - For keystroke-sensitive inputs (slower, fires each key event)
await page.locator('input.search').type('Product', { delay: 100 });
// Modern approach with role-based locators
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('combobox', { name: 'Country' }).selectOption('US');
await page.getByRole('checkbox', { name: 'I agree' }).check();
await page.getByRole('button', { name: 'Submit' }).click();File Uploads
文件上传
javascript
await page.setInputFiles('input[type="file"]', '/path/to/file.pdf');
// Multiple files
await page.setInputFiles('input[type="file"]', [
'/path/to/file1.pdf',
'/path/to/file2.pdf'
]);javascript
await page.setInputFiles('input[type="file"]', '/path/to/file.pdf');
// Multiple files
await page.setInputFiles('input[type="file"]', [
'/path/to/file1.pdf',
'/path/to/file2.pdf'
]);Autocomplete/Search Inputs
自动补全/搜索输入框
javascript
// Type and wait for suggestions (modern approach)
await page.getByPlaceholder('Search products').fill('Product Name');
await expect(page.locator('.suggestions')).toBeVisible();
// Click specific suggestion using role-based locator
await page.getByRole('option', { name: 'Product Name - Premium' }).click();
// Or filter suggestions
await page.locator('.suggestions')
.getByText('Product Name', { exact: false })
.first()
.click();javascript
// Type and wait for suggestions (modern approach)
await page.getByPlaceholder('Search products').fill('Product Name');
await expect(page.locator('.suggestions')).toBeVisible();
// Click specific suggestion using role-based locator
await page.getByRole('option', { name: 'Product Name - Premium' }).click();
// Or filter suggestions
await page.locator('.suggestions')
.getByText('Product Name', { exact: false })
.first()
.click();Screenshot and Debugging
截图与调试
Strategic Screenshots
策略性截图
javascript
// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Element screenshot
await page.locator('.chart').screenshot({ path: 'chart.png' });
// Screenshot on failure (in test)
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({
path: `failure-${testInfo.title}.png`,
fullPage: true
});
}
});javascript
// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Element screenshot
await page.locator('.chart').screenshot({ path: 'chart.png' });
// Screenshot on failure (in test)
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({
path: `failure-${testInfo.title}.png`,
fullPage: true
});
}
});Debug Mode
调试模式
javascript
// Pause execution for debugging
await page.pause();
// Slow down actions for observation
const browser = await chromium.launch({ slowMo: 1000 });javascript
// Pause execution for debugging
await page.pause();
// Slow down actions for observation
const browser = await chromium.launch({ slowMo: 1000 });Common Patterns Reference
常用模式参考
| Task | Pattern |
|---|---|
| Click button | |
| Fill input | |
| Select option | |
| Check checkbox | |
| Wait for element | |
| Assert text | |
| Extract text | |
| Extract multiple | |
| Batch extract | |
| Run JS in page | |
| Take screenshot | |
| Handle new tab | |
| 任务 | 模式 |
|---|---|
| 点击按钮 | |
| 填充输入框 | |
| 选择选项 | |
| 勾选复选框 | |
| 等待元素 | |
| 断言文本 | |
| 提取文本 | |
| 提取多个元素 | |
| 批量提取 | |
| 在页面中运行JS | |
| 截图 | |
| 处理新标签页 | |
Anti-Pattern Checklist
反模式检查清单
Avoid these common mistakes:
- ❌ Using instead of web-first assertions
page.waitForTimeout(5000) - ❌ Using CSS class names or nth-child selectors instead of role-based locators
- ❌ Using instead of
expect(await locator.isVisible()).toBe(true)await expect(locator).toBeVisible() - ❌ Using deprecated - clicks auto-wait now
waitForNavigation() - ❌ Using without asserting count first
locator.all() - ❌ Using when locator should be more specific
first() - ❌ Not handling popups or cookie banners
- ❌ Hardcoding delays instead of waiting for conditions
- ❌ Taking screenshots for data extraction (use evaluate instead)
避免这些常见错误:
- ❌ 使用而非Web优先断言
page.waitForTimeout(5000) - ❌ 使用CSS类名或nth-child选择器而非基于角色的定位器
- ❌ 使用而非
expect(await locator.isVisible()).toBe(true)await expect(locator).toBeVisible() - ❌ 使用已弃用的——现在点击操作会自动等待
waitForNavigation() - ❌ 在未先断言数量的情况下使用
locator.all() - ❌ 当定位器可以更明确时使用
first() - ❌ 未处理弹窗或Cookie横幅
- ❌ 硬编码延迟而非等待条件满足
- ❌ 通过截图提取数据(改用evaluate)
Remember
谨记
Robust automation priorities:
- User-facing locators first - Role, label, placeholder, text (not CSS)
- Web-first assertions - not
await expect(locator).toBeVisible()expect(await ...) - Trust auto-waiting - Don't add manual delays or deprecated patterns
- Strictness is your friend - Fix ambiguous locators, don't use
first() - Batch extraction wisely - Assert count before , use
all()for efficiencyevaluateAll()
Browser automation is inherently asynchronous and timing-dependent. Build in resilience from the start.
健壮自动化的优先级:
- 优先使用面向用户的定位器——角色、标签、占位符、文本(而非CSS)
- 使用Web优先断言——而非
await expect(locator).toBeVisible()expect(await ...) - 信任自动等待机制——不要添加手动延迟或已弃用的模式
- 严格性是你的朋友——修复模糊的定位器,不要使用
first() - 明智地进行批量提取——在使用前先断言数量,使用
all()提升效率evaluateAll()
浏览器自动化本质上是异步且依赖时序的,从一开始就要构建具备韧性的代码。