E2E Testing Automation with Playwright — A Practical Guide
After writing 20+ production E2E test suites across government systems, fintech platforms, and SaaS products, here's what actually works with Playwright — and what wastes your time.
Why Playwright Over Cypress/Selenium
- Multi-browser — Chromium, Firefox, WebKit in one test run. Cypress only does Chromium natively.
- Auto-waiting — No more
sleep(2000). Playwright waits for elements automatically. - Network interception — Mock APIs, block resources, modify responses. Built-in, no plugins.
- Parallel by default — Workers run tests in parallel. 100 tests in 2 minutes, not 20.
- Codegen —
npx playwright codegenrecords your clicks and generates test code.
Setup in 60 Seconds
npm init playwright@latest # Creates: playwright.config.ts, tests/, .github/workflows/ # Run tests npx playwright test # Run with UI mode (best for debugging) npx playwright test --ui # Run specific test npx playwright test login.spec.ts # Generate test from browser recording npx playwright codegen https://your-app.com
The Page Object Pattern
The single most important pattern for maintainable tests. Without it, changing one selector breaks 50 tests. With it, you change one file.
// pages/login.page.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="submit"]');
}
async expectError(message: string) {
await expect(this.page.locator('.error')).toContainText(message);
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test('successful login', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.login('user@test.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
5 Patterns That Save Hours
1. Use data-testid, not CSS selectors
// Bad — breaks when design changes
await page.click('.btn-primary.mt-4');
// Good — stable regardless of styling
await page.click('[data-testid="submit-form"]');
2. API-first authentication
Don't log in through the UI for every test. Use API calls to get auth tokens, then inject them.
// fixtures/auth.ts
test.beforeEach(async ({ page, request }) => {
const res = await request.post('/api/auth/login', {
data: { email: 'test@test.com', password: 'pass' }
});
const { token } = await res.json();
await page.addCookies([{
name: 'auth_token', value: token,
domain: 'localhost', path: '/'
}]);
});
3. Network mocking for flaky APIs
await page.route('**/api/payments', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ success: true, id: 'mock_123' })
});
});
4. Visual regression with screenshots
// First run creates baseline, subsequent runs compare
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100
});
5. Retry flaky assertions, not flaky tests
// Bad — retries the entire test
test('...', { retries: 3 }, async () => { ... });
// Good — retries just the assertion with auto-wait
await expect(page.locator('.toast'))
.toBeVisible({ timeout: 5000 });
CI/CD Integration (GitHub Actions)
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Our Testing Checklist
- Smoke tests first — Can the user log in, see the dashboard, perform the core action?
- Happy paths second — Complete user journeys (sign up → use feature → check result)
- Error paths third — Invalid input, network errors, permission denied
- Edge cases last — Empty states, very long text, concurrent users, browser-specific bugs
This order matters. If smoke tests fail, nothing else matters. If happy paths work, you have a shippable product. Error handling and edge cases are polish.
We reduced our client's manual testing time by 70% with this approach. The test suite runs in 4 minutes on CI and catches 95% of regressions before human QA touches the build.
Need test automation for your project?
We build Playwright test suites that run in CI/CD and catch bugs before production.
Get a Free QA Assessment