From dev-infrastructure-skills
This skill should be used when the user asks to "write E2E tests", "set up Playwright", "write browser tests", "test user flows", "automate browser testing", "fix flaky tests", "debug E2E failures", "add end-to-end testing", or needs guidance on Playwright locators, fixtures, page objects, accessibility testing, visual regression, mobile testing, or CI/CD test infrastructure.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dev-infrastructure-skills:playwright-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
End-to-end testing patterns for web applications using Playwright. Covers test writing, debugging, CI/CD integration, and specialized testing scenarios.
End-to-end testing patterns for web applications using Playwright. Covers test writing, debugging, CI/CD integration, and specialized testing scenarios.
Always prefer locators that reflect how users interact with the page, not implementation details:
// BEST: Role-based (most resilient)
page.getByRole('button', { name: 'Submit' })
page.getByRole('heading', { name: 'Welcome' })
page.getByRole('textbox', { name: 'Email' })
// GOOD: Label/placeholder-based
page.getByLabel('Email address')
page.getByPlaceholder('Enter your email')
page.getByText('Sign in')
// AVOID: Implementation-coupled
page.locator('#submit-btn')
page.locator('.btn-primary')
page.locator('[data-testid="submit"]') // Use only as last resort
getByRole() — accessible role + namegetByLabel() — form field labelsgetByPlaceholder() — input placeholdersgetByText() — visible text contentgetByAltText() — image alt textgetByTitle() — title attributegetByTestId() — last resort for complex elementsPlaywright auto-waits for assertions. Never use manual waits.
// BAD: Manual wait
await page.waitForTimeout(2000);
expect(await page.textContent('.status')).toBe('Done');
// GOOD: Auto-waiting assertion
await expect(page.getByText('Done')).toBeVisible();
// GOOD: Wait for specific state
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await expect(page.getByRole('alert')).toContainText('Saved successfully');
page.waitForTimeout()If you find yourself adding timeouts, the test is fragile. Use web-first assertions or wait for specific events:
// Instead of waitForTimeout, wait for the actual condition
await page.waitForResponse(resp => resp.url().includes('/api/save') && resp.status() === 200);
await expect(page.getByText('Saved')).toBeVisible();
Encapsulate page interactions in reusable classes:
// models/login-page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
async expectError(message: string) {
await expect(this.page.getByRole('alert')).toContainText(message);
}
}
// tests/login.spec.ts
test('shows error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpassword');
await loginPage.expectError('Invalid credentials');
});
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './models/login-page';
type Fixtures = {
loginPage: LoginPage;
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
await use(page);
},
});
Each test should be independent. Never depend on state from previous tests.
// GOOD: Each test sets up its own state
test('user can create a project', async ({ authenticatedPage }) => {
await authenticatedPage.getByRole('button', { name: 'New Project' }).click();
await authenticatedPage.getByLabel('Project name').fill('Test Project');
await authenticatedPage.getByRole('button', { name: 'Create' }).click();
await expect(authenticatedPage.getByText('Test Project')).toBeVisible();
});
# Run with inspector
npx playwright test --debug
# Run specific test with UI
npx playwright test login.spec.ts --ui
// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry', // Captures trace on failure
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Common causes and fixes:
| Symptom | Cause | Fix |
|---|---|---|
| Passes locally, fails in CI | Timing/network speed | Use web-first assertions |
| Intermittent failures | Race condition | Wait for specific state, not timeouts |
| Different results each run | Shared test state | Isolate tests, use fresh data |
| Works in headed, fails headless | Viewport/animation differences | Set explicit viewport, disable animations |
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 4 : undefined,
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
});
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
import AxeBuilder from '@axe-core/playwright';
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
import { devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 14'] } },
{ name: 'Tablet', use: { ...devices['iPad Pro 11'] } },
],
});
test('handles API error gracefully', async ({ page }) => {
await page.route('**/api/items', route =>
route.fulfill({ status: 500, body: 'Server Error' })
);
await page.goto('/items');
await expect(page.getByText('Failed to load items')).toBeVisible();
});
waitForTimeout — all waits are condition-basedFor detailed patterns on each topic, see references/.
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub moxywolfllc/moxywolf-plugins --plugin dev-infrastructure-skills