playwright-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright 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:
  1. Role-based locators (best - user-centric)
    javascript
    await page.getByRole('button', { name: 'Submit' }).click();
    await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
  2. Other user-facing locators
    javascript
    await page.getByLabel('Password').fill('secret');
    await page.getByPlaceholder('Search...').fill('query');
    await page.getByText('Submit Order').click();
  3. 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' }
  4. 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:
  1. 基于角色的定位器(最优 - 以用户为中心)
    javascript
    await page.getByRole('button', { name: 'Submit' }).click();
    await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
  2. 其他面向用户的定位器
    javascript
    await page.getByLabel('Password').fill('secret');
    await page.getByPlaceholder('Search...').fill('query');
    await page.getByText('Submit Order').click();
  3. 测试ID属性(显式约定)
    javascript
    // Default uses data-testid
    await page.getByTestId('submit-button').click();
    
    // Can customize in playwright.config.ts:
    // use: { testIdAttribute: 'data-pw' }
  4. 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 this

Locator 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 end

Explicit 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

常用模式参考

TaskPattern
Click button
await page.getByRole('button', { name: 'Text' }).click()
Fill input
await page.getByLabel('Field').fill('value')
Select option
await page.getByRole('combobox').selectOption('value')
Check checkbox
await page.getByRole('checkbox', { name: 'Label' }).check()
Wait for element
await expect(page.locator('.el')).toBeVisible()
Assert text
await expect(page.locator('.el')).toHaveText('text')
Extract text
const text = await page.locator('.el').textContent()
Extract multiple
await expect(locator).toHaveCount(5); const els = await locator.all()
Batch extract
const data = await page.locator('.el').evaluateAll(els => ...)
Run JS in page
await page.evaluate(() => /* JS code */)
Take screenshot
await page.screenshot({ path: 'shot.png' })
Handle new tab
const newPage = await context.waitForEvent('page', () => page.click('a'))
任务模式
点击按钮
await page.getByRole('button', { name: 'Text' }).click()
填充输入框
await page.getByLabel('Field').fill('value')
选择选项
await page.getByRole('combobox').selectOption('value')
勾选复选框
await page.getByRole('checkbox', { name: 'Label' }).check()
等待元素
await expect(page.locator('.el')).toBeVisible()
断言文本
await expect(page.locator('.el')).toHaveText('text')
提取文本
const text = await page.locator('.el').textContent()
提取多个元素
await expect(locator).toHaveCount(5); const els = await locator.all()
批量提取
const data = await page.locator('.el').evaluateAll(els => ...)
在页面中运行JS
await page.evaluate(() => /* JS code */)
截图
await page.screenshot({ path: 'shot.png' })
处理新标签页
const newPage = await context.waitForEvent('page', () => page.click('a'))

Anti-Pattern Checklist

反模式检查清单

Avoid these common mistakes:
  • ❌ Using
    page.waitForTimeout(5000)
    instead of web-first assertions
  • ❌ Using CSS class names or nth-child selectors instead of role-based locators
  • ❌ Using
    expect(await locator.isVisible()).toBe(true)
    instead of
    await expect(locator).toBeVisible()
  • ❌ Using deprecated
    waitForNavigation()
    - clicks auto-wait now
  • ❌ Using
    locator.all()
    without asserting count first
  • ❌ Using
    first()
    when locator should be more specific
  • ❌ Not handling popups or cookie banners
  • ❌ Hardcoding delays instead of waiting for conditions
  • ❌ Taking screenshots for data extraction (use evaluate instead)
避免这些常见错误:
  • ❌ 使用
    page.waitForTimeout(5000)
    而非Web优先断言
  • ❌ 使用CSS类名或nth-child选择器而非基于角色的定位器
  • ❌ 使用
    expect(await locator.isVisible()).toBe(true)
    而非
    await expect(locator).toBeVisible()
  • ❌ 使用已弃用的
    waitForNavigation()
    ——现在点击操作会自动等待
  • ❌ 在未先断言数量的情况下使用
    locator.all()
  • ❌ 当定位器可以更明确时使用
    first()
  • ❌ 未处理弹窗或Cookie横幅
  • ❌ 硬编码延迟而非等待条件满足
  • ❌ 通过截图提取数据(改用evaluate)

Remember

谨记

Robust automation priorities:
  1. User-facing locators first - Role, label, placeholder, text (not CSS)
  2. Web-first assertions -
    await expect(locator).toBeVisible()
    not
    expect(await ...)
  3. Trust auto-waiting - Don't add manual delays or deprecated patterns
  4. Strictness is your friend - Fix ambiguous locators, don't use
    first()
  5. Batch extraction wisely - Assert count before
    all()
    , use
    evaluateAll()
    for efficiency
Browser automation is inherently asynchronous and timing-dependent. Build in resilience from the start.
健壮自动化的优先级:
  1. 优先使用面向用户的定位器——角色、标签、占位符、文本(而非CSS)
  2. 使用Web优先断言——
    await expect(locator).toBeVisible()
    而非
    expect(await ...)
  3. 信任自动等待机制——不要添加手动延迟或已弃用的模式
  4. 严格性是你的朋友——修复模糊的定位器,不要使用
    first()
  5. 明智地进行批量提取——在使用
    all()
    前先断言数量,使用
    evaluateAll()
    提升效率
浏览器自动化本质上是异步且依赖时序的,从一开始就要构建具备韧性的代码。