playwright-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright Best Practices

Playwright最佳实践

Overview

概述

This skill provides guidance on writing reliable, maintainable Playwright automation code based on official best practices. It covers element operations, waiting strategies, navigation patterns, and common anti-patterns to avoid.
本技能基于官方最佳实践,提供编写可靠、可维护的Playwright自动化代码的指导。内容涵盖元素操作、等待策略、导航模式以及需要避免的常见反模式。

Core Principles

核心原则

1. Always Use Locators with Auto-waiting

1. 始终使用具备自动等待功能的Locator

Playwright Locators automatically wait and retry until elements are actionable:
typescript
// ✅ Recommended: Locator with auto-waiting
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Username').fill('john');

// ❌ Avoid: Direct DOM manipulation
await page.evaluate(() => document.querySelector('button').click());
Playwright Locators会自动等待并重试,直到元素可操作:
typescript
// ✅ 推荐:具备自动等待的Locator
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Username').fill('john');

// ❌ 避免:直接DOM操作
await page.evaluate(() => document.querySelector('button').click());

2. Prefer User-Facing Attributes

2. 优先使用面向用户的属性

Prioritize locators based on how users perceive the page:
typescript
// Priority order:
// 1. Role-based (best for accessibility)
await page.getByRole('button', { name: 'Sign in' });

// 2. Label-based (best for forms)
await page.getByLabel('Email address');

// 3. Text-based
await page.getByText('Submit');

// 4. Test ID
await page.getByTestId('submit-button');

// 5. CSS/XPath (last resort)
await page.locator('.btn-primary');
优先选择基于用户对页面感知的定位器:
typescript
// 优先级顺序:
// 1. 基于角色(最适合无障碍访问)
await page.getByRole('button', { name: 'Sign in' });

// 2. 基于标签(最适合表单)
await page.getByLabel('Email address');

// 3. 基于文本
await page.getByText('Submit');

// 4. 测试ID
await page.getByTestId('submit-button');

// 5. CSS/XPath(最后选择)
await page.locator('.btn-primary');

3. Trust Auto-waiting, Minimize Explicit Waits

3. 信任自动等待,尽量减少显式等待

Locators automatically check actionability before operations:
typescript
// ✅ Recommended: Action includes waiting
await page.getByRole('button').click();

// ⚠️ Usually unnecessary
await page.waitForSelector('button');
await page.locator('button').click();
定位器会在操作前自动检查可操作性:
typescript
// ✅ 推荐:操作已包含等待逻辑
await page.getByRole('button').click();

// ⚠️ 通常无必要
await page.waitForSelector('button');
await page.locator('button').click();

Common Workflows

常见工作流

Element Operations

元素操作

Click:
typescript
await page.getByRole('button', { name: 'Next' }).click();
Fill text:
typescript
await page.getByLabel('Email').fill('user@example.com');
Select option:
typescript
await page.getByLabel('Country').selectOption({ label: 'Japan' });
Check/uncheck:
typescript
await page.getByLabel('I agree').check();
点击:
typescript
await page.getByRole('button', { name: 'Next' }).click();
填充文本:
typescript
await page.getByLabel('Email').fill('user@example.com');
选择选项:
typescript
await page.getByLabel('Country').selectOption({ label: 'Japan' });
勾选/取消勾选:
typescript
await page.getByLabel('I agree').check();

Navigation Patterns

导航模式

Same-page navigation:
typescript
// ✅ Recommended: Click and wait for next page element
await page.getByRole('link', { name: 'Next' }).click();
await page.waitForSelector('#next-page-element', { timeout: 30 * 1000 }); // example wait time

// Or use conditional selector for different destination pages
const waitSelector = condition === 'A'
  ? '#page-a-element'
  : '#page-b-element';
await page.waitForSelector(waitSelector, { timeout: 30 * 1000 }); // example wait time

// Or use waitForURL if URL pattern is predictable
await page.click('button');
await page.waitForURL('**/next-page');
New tab/window:
typescript
const [newPage] = await Promise.all([
  page.context().waitForEvent('page'),
  page.getByRole('link', { name: 'Open in new tab' }).click()
]);
await newPage.waitForLoadState();
同页面导航:
typescript
// ✅ 推荐:点击后等待下一页元素
await page.getByRole('link', { name: 'Next' }).click();
await page.waitForSelector('#next-page-element', { timeout: 30 * 1000 }); // 示例等待时间

// 或者针对不同目标页面使用条件定位器
const waitSelector = condition === 'A'
  ? '#page-a-element'
  : '#page-b-element';
await page.waitForSelector(waitSelector, { timeout: 30 * 1000 }); // 示例等待时间

// 如果URL模式可预测,也可使用waitForURL
await page.click('button');
await page.waitForURL('**/next-page');
新标签页/窗口:
typescript
const [newPage] = await Promise.all([
  page.context().waitForEvent('page'),
  page.getByRole('link', { name: 'Open in new tab' }).click()
]);
await newPage.waitForLoadState();

Form Interactions

表单交互

typescript
// Complete form workflow
await page.getByLabel('Username').fill('john');
await page.getByLabel('Password').fill('secret');
await page.getByRole('checkbox', { name: 'Remember me' }).check();
await page.getByLabel('Country').selectOption('Japan');
await page.getByRole('button', { name: 'Submit' }).click();

// Verify submission
await expect(page.getByText('Success')).toBeVisible();
typescript
// 完整表单工作流
await page.getByLabel('Username').fill('john');
await page.getByLabel('Password').fill('secret');
await page.getByRole('checkbox', { name: 'Remember me' }).check();
await page.getByLabel('Country').selectOption('Japan');
await page.getByRole('button', { name: 'Submit' }).click();

// 验证提交结果
await expect(page.getByText('Success')).toBeVisible();

Anti-Patterns to Avoid

需要避免的反模式

❌ waitForNavigation (Deprecated)

❌ waitForNavigation(已弃用)

typescript
// ❌ Avoid: Deprecated API
const navigationPromise = page.waitForNavigation();
await page.click('button');
await navigationPromise;

// ✅ Use instead: waitForURL or waitForSelector
await page.click('button');
await page.waitForURL('**/next-page');
// or
await page.waitForSelector('#next-page-element');
typescript
// ❌ 避免:已弃用的API
const navigationPromise = page.waitForNavigation();
await page.click('button');
await navigationPromise;

// ✅ 替代方案:使用waitForURL或waitForSelector
await page.click('button');
await page.waitForURL('**/next-page');
// 或者
await page.waitForSelector('#next-page-element');

❌ networkidle (Unreliable)

❌ networkidle(不可靠)

typescript
// ❌ Avoid: Can complete before page is ready
await page.waitForLoadState('networkidle');

// ✅ Use instead: Wait for specific elements
await page.waitForSelector('#content-loaded');
// or
await page.waitForLoadState('load');
typescript
// ❌ 避免:可能在页面准备好前完成
await page.waitForLoadState('networkidle');

// ✅ 替代方案:等待特定元素
await page.waitForSelector('#content-loaded');
// 或者
await page.waitForLoadState('load');

❌ page.evaluate() for Clicks

❌ 使用page.evaluate()执行点击

typescript
// ❌ Avoid: Bypasses actionability checks
await page.evaluate(() => document.querySelector('button').click());

// ✅ Use instead: Locator click
await page.locator('button').click();
typescript
// ❌ 避免:绕过可操作性检查
await page.evaluate(() => document.querySelector('button').click());

// ✅ 替代方案:使用Locator点击
await page.locator('button').click();

❌ Unnecessary Promise.all

❌ 不必要的Promise.all

typescript
// ❌ Unnecessary: Playwright auto-waits for navigation
await Promise.all([
  page.waitForNavigation(),
  page.click('button')
]);

// ✅ Simpler: Just click
await page.click('button');
typescript
// ❌ 无必要:Playwright会自动等待导航
await Promise.all([
  page.waitForNavigation(),
  page.click('button')
]);

// ✅ 更简洁:直接点击
await page.click('button');

Actionability Checks

可操作性检查

Playwright automatically verifies these conditions before actions:
ActionVisibleStableReceives EventsEnabledEditable
click()-
fill()--
check()-
selectOption()---
hover()--
Definitions:
  • Visible: Has bounding box, not
    visibility:hidden
  • Stable: Same position for 2+ frames (animation complete)
  • Receives Events: Not covered by other elements
  • Enabled: No
    disabled
    attribute
  • Editable: Enabled and not
    readonly
Playwright会在执行操作前自动验证以下条件:
操作可见稳定可接收事件启用状态可编辑
click()-
fill()--
check()-
selectOption()---
hover()--
定义:
  • 可见:具有边界框,且未设置
    visibility:hidden
  • 稳定:连续2帧以上位置不变(动画完成)
  • 可接收事件:未被其他元素遮挡
  • 启用状态:无
    disabled
    属性
  • 可编辑:处于启用状态且未设置
    readonly

When to Use Explicit Waits

何时使用显式等待

Explicit waits are needed in these cases:
typescript
// 1. Before using locator.all() (doesn't auto-wait)
await page.getByRole('listitem').first().waitFor();
const items = await page.getByRole('listitem').all();

// 2. Waiting for element to disappear
await page.locator('.loading').waitFor({ state: 'hidden' });

// 3. Waiting for element to detach
await page.locator('.modal').waitFor({ state: 'detached' });

// 4. Conditional page destinations
const waitSelector = condition ? '#page-a' : '#page-b';
await page.waitForSelector(waitSelector, { timeout: 30 * 1000 }); // example wait time
在以下场景中需要使用显式等待:
typescript
// 1. 使用locator.all()之前(该方法无自动等待)
await page.getByRole('listitem').first().waitFor();
const items = await page.getByRole('listitem').all();

// 2. 等待元素消失
await page.locator('.loading').waitFor({ state: 'hidden' });

// 3. 等待元素脱离DOM
await page.locator('.modal').waitFor({ state: 'detached' });

// 4. 条件性页面目标
const waitSelector = condition ? '#page-a' : '#page-b';
await page.waitForSelector(waitSelector, { timeout: 30 * 1000 }); // 示例等待时间

Advanced References

进阶参考

For detailed information on specific topics, see:
  • anti-patterns.md: Detailed explanation of deprecated patterns and why to avoid them
  • locators.md: Comprehensive guide to locator strategies and selection
  • actions.md: Detailed action methods and their behavior
如需了解特定主题的详细信息,请查看:
  • anti-patterns.md:已弃用模式的详细说明及规避原因
  • locators.md:定位器策略与选择的全面指南
  • actions.md:操作方法及其行为的详细说明

Official Documentation

官方文档