Playwright E2E practices that survive real teams
End-to-end tests are expensive: slow, brittle if written carelessly, and yet irreplaceable for critical paths (checkout, signup, permissions). Playwright’s API and tooling make disciplined E2E realistic—if you adopt a few conventions.
Prefer role and text selectors
await page.getByRole('button', { name: /submit payment/i }).click();
await expect(page.getByRole('heading', { name: 'Receipt' })).toBeVisible();
Avoid CSS chains tied to styling (div.flex > span:nth(3)). They break on harmless refactors.
Fixtures for auth and seed data
Centralize login in a fixture so specs start authenticated without copy-pasting page.goto('/login') flows.
// playwright/fixtures.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{ authedPage: Page }>({
authedPage: async ({ page }, use) => {
await page.goto('/login');
// ...token injection or form login
await use(page);
},
});
Use API seeding when possible instead of driving UI for every prerequisite—it cuts runtime and flakiness.
No arbitrary timeouts
Replace waitForTimeout(3000) with assertions that retry: expect(locator).toBeVisible() or page.waitForResponse keyed to the network call you care about.
Parallelism and sharding
CI should run npx playwright test --shard=1/4 across workers. Combine with fullyParallel: true in config when tests do not share mutable server state.
Traces and reports
Enable trace: 'retain-on-failure' and upload playwright-report as a CI artifact. First step when debugging a red build on CI should be opening the trace zip—not re-running locally blindly.
Summary
Reliable Playwright suites lean on accessible selectors, fixtures, deterministic waits, and artifacts. Treat E2E as a small, high-value layer—not a replacement for unit and integration tests.