Playwright E2E Tester
Overview
Expert in end-to-end testing with Playwright, the modern cross-browser testing framework. Specializes in test generation, page object patterns, visual regression testing, and CI/CD integration. Handles complex testing scenarios including authentication flows, API mocking, and mobile emulation.
When to Use
- Setting up Playwright in a new or existing project
- Writing E2E tests for critical user flows
- Debugging flaky tests or test failures
- Implementing visual regression testing
- Configuring Playwright for CI/CD pipelines
- Migrating from Cypress, Selenium, or Puppeteer
Decision Points
Locator Strategy Selection
Is element semantic (button, heading, form control)?
├─ YES → Use role-based locators (getByRole, getByLabel)
│ └─ Expected to change frequently?
│ ├─ NO → Stop here (most stable)
│ └─ YES → Add getByTestId as backup
└─ NO → Is element purely presentational?
├─ YES → Use getByTestId (add data-testid attribute)
│ └─ Can't modify markup?
│ └─ Use CSS selector (last resort, document fragility)
└─ NO → Use getByText for content-based selection
Wait Strategy Decision
Action type:
├─ Navigation (page.goto, click link) → Use waitForURL()
├─ Element appears/disappears → Use waitForSelector() or visibility assertions
├─ API response affects UI → Use waitForResponse() then element assertion
├─ Animation/transition → Use waitForFunction() with custom condition
└─ Network request completion → Use page.route() with route.fulfill()
Test Organization
Test complexity:
├─ Single page interaction → Inline test with direct selectors
├─ Multi-step flow → Use Page Object Model pattern
├─ Cross-page workflow → Use fixtures for shared state
└─ Multi-app integration → Use projects with different configs
Failure Modes
Stale Element Reference
Symptoms: Error: Element is not attached to the DOM
Detection Rule: If you see detachment errors during dynamic content updates
Fix: Replace element variables with fresh locator calls:
// BAD: Storing element reference
const button = page.locator('button');
await button.click(); // May fail if DOM updated
// GOOD: Fresh locator each time
await page.locator('button').click();
Timeout Hell
Symptoms: Tests fail with "Timeout exceeded" in CI but pass locally Detection Rule: If tests have inconsistent CI failures with 30s+ timeouts Fix: Implement explicit waits with proper conditions:
// BAD: Blind timeout increase
await page.waitForTimeout(5000);
// GOOD: Wait for specific condition
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
await expect(page.locator('[data-testid="results"]')).toBeVisible();
Race Condition Rapids
Symptoms: Intermittent failures where elements "aren't ready yet" Detection Rule: If test flakiness correlates with slow network/CPU Fix: Chain waits to ensure proper sequencing:
// BAD: Assuming immediate availability
await page.click('#submit');
await page.fill('#new-field', 'value'); // May fail
// GOOD: Wait for UI state transition
await page.click('#submit');
await page.waitForSelector('#new-field:not([disabled])');
await page.fill('#new-field', 'value');
Selector Fragility Syndrome
Symptoms: Tests break when CSS classes or DOM structure changes Detection Rule: If tests fail after frontend refactoring without feature changes Fix: Migrate to semantic locators:
// BAD: Structural dependency
await page.click('.header > .nav > .item:nth-child(3)');
// GOOD: Semantic meaning
await page.getByRole('navigation').getByRole('link', { name: 'Products' }).click();
Screenshot Drift Disease
Symptoms: Visual regression tests fail due to minor rendering differences Detection Rule: If screenshot tests fail in CI with <5% pixel differences Fix: Configure appropriate tolerances and masks:
await expect(page).toHaveScreenshot('page.png', {
maxDiffPixelRatio: 0.01,
mask: [page.locator('[data-testid="dynamic-timestamp"]')],
animations: 'disabled'
});
Worked Examples
Authentication Flow Test Implementation
Scenario: Test user login with error handling and success validation
Expert Approach:
import { test, expect } from '@playwright/test';
test.describe('User Authentication', () => {
test('should handle complete login flow', async ({ page }) => {
// Decision: Use Page Object for multi-step flow
const loginPage = new LoginPage(page);
await loginPage.goto();
// Decision: Test error state first (negative case)
await loginPage.signIn('invalid@email.com', 'wrongpass');
// Wait strategy: Error message should appear
await expect(page.getByRole('alert')).toContainText('Invalid credentials');
// Decision: Clear state before positive test
await loginPage.clearForm();
// Decision: Use valid test data
await loginPage.signIn('user@example.com', 'validpass');
// Wait strategy: Navigation indicates success
await page.waitForURL('/dashboard');
// Verification: Check authenticated state
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByText('Welcome back, John')).toBeVisible();
});
});
class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
// Wait for form to be interactive
await this.page.waitForSelector('form[data-testid="login-form"]');
}
async signIn(email: string, password: string) {
// Decision: Use semantic locators
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign In' }).click();
}
async clearForm() {
await this.page.getByLabel('Email').clear();
await this.page.getByLabel('Password').clear();
}
}
Novice would miss: Error state testing, proper wait strategies, form state management Expert catches: Complete flow coverage, semantic locators, defensive waits
Quality Gates
Test implementation checklist:
- All user interactions use semantic locators (getByRole, getByLabel, getByText)
- No hard waits (page.waitForTimeout) - only conditional waits
- Error states and edge cases are tested, not just happy path
- Tests pass consistently in CI (5+ consecutive runs without flakes)
- Each test is independent and can run in any order
- Page Object Model used for multi-step flows (3+ interactions)
- Visual regression tests have appropriate tolerance thresholds (<2% pixel diff)
- API mocking implemented for external dependencies
- Authentication state properly managed via fixtures
- Test coverage includes critical user journeys (login, checkout, etc.)
NOT-FOR Boundaries
Don't use this skill for:
- Unit testing individual functions → Use
vitest-testing-patternsinstead - API contract testing → Use
api-architectfor endpoint validation - Performance testing → Use dedicated load testing tools like k6
- Security penetration testing → Use specialized security testing tools
- Cross-browser compatibility issues → Use browser-specific debugging tools
Delegate to other skills:
- For CI/CD pipeline setup → Use
github-actions-pipeline-builder - For accessibility auditing → Use
accessibility-auditor - For component testing → Use
vitest-testing-patterns