install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/test-playwright-patterns" ~/.claude/skills/intense-visions-harness-engineering-test-playwright-patterns && rm -rf "$T"
manifest:
agents/skills/claude-code/test-playwright-patterns/SKILL.mdsource content
Test Playwright Patterns
Write maintainable Playwright tests using page objects, fixtures, and parallel execution
When to Use
- Writing E2E tests that remain maintainable as the application grows
- Abstracting page interactions into reusable page objects
- Testing complex user flows (multi-step forms, navigation, authentication)
- Debugging flaky tests and improving test reliability
Instructions
- Page Object Model — encapsulate page interactions:
// e2e/pages/login-page.ts import { Page, Locator, expect } from '@playwright/test'; export class LoginPage { readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; constructor(private page: Page) { this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Log in' }); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } }
- Use locators — prefer accessible selectors:
// Best — accessible page.getByRole('button', { name: 'Submit' }); page.getByLabel('Email address'); page.getByText('Welcome back'); page.getByPlaceholder('Search...'); // Acceptable — test IDs page.getByTestId('user-avatar'); // Avoid — brittle page.locator('.btn-primary'); page.locator('#submit-form'); page.locator('div > span:nth-child(2)');
- Test user flows, not individual elements:
import { test, expect } from '@playwright/test'; test('user can create and publish a post', async ({ page }) => { await page.goto('/posts/new'); await page.getByLabel('Title').fill('My First Post'); await page.getByLabel('Content').fill('Hello, world!'); await page.getByRole('button', { name: 'Save draft' }).click(); await expect(page.getByText('Draft saved')).toBeVisible(); await page.getByRole('button', { name: 'Publish' }).click(); await expect(page).toHaveURL(/\/posts\/[\w-]+$/); await expect(page.getByRole('heading', { name: 'My First Post' })).toBeVisible(); });
- Wait for network and state — use auto-waiting:
// Playwright auto-waits for elements to be actionable await page.getByRole('button', { name: 'Submit' }).click(); // Waits for button to be enabled // Wait for navigation await Promise.all([ page.waitForURL('/dashboard'), page.getByRole('button', { name: 'Log in' }).click(), ]); // Wait for API response const responsePromise = page.waitForResponse('/api/users'); await page.getByRole('button', { name: 'Load users' }).click(); const response = await responsePromise; expect(response.status()).toBe(200);
- Handle dialogs and popups:
page.on('dialog', (dialog) => dialog.accept()); await page.getByRole('button', { name: 'Delete' }).click(); await expect(page.getByText('Item deleted')).toBeVisible();
- Visual regression with screenshots:
test('homepage matches snapshot', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixels: 100, }); });
- Parallel test isolation:
// Each test gets its own browser context test('user A sees their data', async ({ page }) => { /* ... */ }); test('user B sees their data', async ({ page }) => { /* ... */ }); // These run in parallel without interference
- Tag and filter tests:
test('critical: checkout flow', { tag: '@critical' }, async ({ page }) => { // ... }); // Run only critical tests // npx playwright test --grep @critical
Details
Playwright tests run against real browsers with full DOM, network, and JavaScript execution. They are the closest thing to manual testing that can be automated.
Auto-waiting: Playwright automatically waits for elements to be visible, enabled, and stable before interacting. This eliminates most explicit waits and reduces flakiness compared to Selenium or Cypress.
Locator best practices:
— always first choice. Forces accessible markupgetByRole
— for form inputs, anchored to their labelgetByLabel
— for visible text contentgetByText
— when no accessible name exists. AddgetByTestId
attributesdata-testid- CSS/XPath — last resort, only for complex DOM structures
Debugging tools:
— interactive test runner with time-travel debuggingnpx playwright test --ui
— step through with browser DevToolsnpx playwright test --debug
— replay recorded tracesnpx playwright show-trace trace.zip
Handling flakiness:
- Use locators (auto-waiting) instead of explicit waits
- Wait for specific conditions, not arbitrary timeouts
- Ensure test data isolation (each test creates its own data)
- Use
in CI as a safety net, but investigate repeated flakesretries: 2
Trade-offs:
- Page objects improve maintainability — but add abstraction layers
- Parallel execution speeds up suites — but requires data isolation between tests
- Visual regression catches CSS bugs — but screenshot diffs are sensitive to rendering differences across environments
- Multi-browser testing improves confidence — but triples CI time
Source
https://playwright.dev/docs/test-patterns
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.