| name | e2e-patterns |
| type | skill |
| description | Write robust E2E tests with Playwright — page objects, resilient waiting, auth, and CI integration. |
| related-rules | ["test-strategy.md","flakiness-policy.md"] |
| allowed-tools | Read, Write, Edit, Bash |
E2E Testing Patterns (Playwright) Skill
Expertise: Playwright page objects, resilient locators, auth fixtures, parallel execution, CI configuration.
Page Object Model
export class OrderPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get orderItems() { return this.page.getByRole('listitem', { name: /order item/i }); }
get submitButton() { return this.page.getByRole('button', { name: 'Place order' }); }
get confirmationMessage() { return this.page.getByText('Order confirmed'); }
async addItem(productName: string, quantity: number) {
await this.page.getByLabel('Product').selectOption(productName);
await this.page.getByLabel('Quantity').fill(String(quantity));
await this.page.getByRole('button', { name: 'Add to cart' }).click();
}
async checkout(address: Address) {
await this.page.getByLabel('Street').fill(address.street);
await this.page.getByLabel('City').fill(address.city);
await this.submitButton.click();
await expect(this.confirmationMessage).toBeVisible({ timeout: 10000 });
}
}
Resilient Waiting — Never Use sleep
await page.waitForTimeout(2000);
await page.waitForURL('**/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 201),
page.getByRole('button', { name: 'Place order' }).click(),
]);
await expect(page.getByRole('status')).toHaveText('Processing...', { timeout: 5000 });
await expect(page.getByRole('status')).toHaveText('Complete', { timeout: 30000 });
Authentication Fixture (Reuse Across Tests)
import { test as base } from '@playwright/test';
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'tests/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export async function setupAuth() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
page.().(process..!);
page.(, { : }).();
page.();
page.().({ : });
browser.();
}
Playwright Config (CI-Ready)
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : 2,
timeout: 30000,
reporter: [
['html', { outputFolder: 'test-results/html' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'setup', testMatch: /global.setup.ts/ },
{
name: 'chromium',
dependencies: ['setup'],
use: { ...devices['Desktop Chrome'], : },
},
],
});
Flakiness Prevention Checklist
Locator Priority (Most to Least Resilient)
1. getByRole() — semantic, tests a11y
2. getByLabel() — form elements
3. getByText() — visible text
4. getByTestId() — data-testid attribute (explicit contract)
5. locator('css') — fragile, avoid
6. locator('xpath') — most fragile, never use