Claude Code Plugins

Community-maintained marketplace

Feedback

playwright-best-practices

@Suntory-N-Water/sui-blog
1
0

Playwright automation best practices for web scraping and testing. Use when writing or reviewing Playwright code, especially for element operations (click, fill, select), waiting strategies (waitForSelector, waitForURL, waitForLoadState), navigation patterns, and locator usage. Apply when encountering unstable tests, race conditions, or needing guidance on avoiding deprecated patterns like waitForNavigation or networkidle.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name playwright-best-practices
description Playwright automation best practices for web scraping and testing. Use when writing or reviewing Playwright code, especially for element operations (click, fill, select), waiting strategies (waitForSelector, waitForURL, waitForLoadState), navigation patterns, and locator usage. Apply when encountering unstable tests, race conditions, or needing guidance on avoiding deprecated patterns like waitForNavigation or networkidle.

Playwright Best Practices

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.

Core Principles

1. Always Use Locators with Auto-waiting

Playwright Locators automatically wait and retry until elements are actionable:

// ✅ 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());

2. Prefer User-Facing Attributes

Prioritize locators based on how users perceive the page:

// 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');

3. Trust Auto-waiting, Minimize Explicit Waits

Locators automatically check actionability before operations:

// ✅ Recommended: Action includes waiting
await page.getByRole('button').click();

// ⚠️ Usually unnecessary
await page.waitForSelector('button');
await page.locator('button').click();

Common Workflows

Element Operations

Click:

await page.getByRole('button', { name: 'Next' }).click();

Fill text:

await page.getByLabel('Email').fill('user@example.com');

Select option:

await page.getByLabel('Country').selectOption({ label: 'Japan' });

Check/uncheck:

await page.getByLabel('I agree').check();

Navigation Patterns

Same-page navigation:

// ✅ 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:

const [newPage] = await Promise.all([
  page.context().waitForEvent('page'),
  page.getByRole('link', { name: 'Open in new tab' }).click()
]);
await newPage.waitForLoadState();

Form Interactions

// 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();

Anti-Patterns to Avoid

❌ waitForNavigation (Deprecated)

// ❌ 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');

❌ networkidle (Unreliable)

// ❌ 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');

❌ page.evaluate() for Clicks

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

// ✅ Use instead: Locator click
await page.locator('button').click();

❌ Unnecessary Promise.all

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

// ✅ Simpler: Just click
await page.click('button');

Actionability Checks

Playwright automatically verifies these conditions before actions:

Action Visible Stable Receives Events Enabled Editable
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

When to Use Explicit Waits

Explicit waits are needed in these cases:

// 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

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

Official Documentation