iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

E2E Tests

End-to-end tests drive a real browser through your full stack — UI, API, database — and verify whole user flows. Playwright is the modern default: fast, cross-browser, with auto-waits, network mocking, and parallel sharding for CI.

Playwright in CI, fixtures, sharding

EXAMPLE
# 1) Install
npm init playwright@latest
# Wizard generates playwright.config.ts, tests/, GitHub Action.

# 2) playwright.config.ts (typical)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    testDir: './tests',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: process.env.CI ? 4 : undefined,
    reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
    use: {
        baseURL: process.env.E2E_URL ?? 'http://localhost:3000',
        trace:   'retain-on-failure',
        screenshot: 'only-on-failure',
        video:      'retain-on-failure',
        actionTimeout:    10_000,
        navigationTimeout: 30_000,
    },
    projects: [
        { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
        { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
        { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
        { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
    ],
    webServer: process.env.CI ? undefined : {
        command: 'npm run dev',
        url:     'http://localhost:3000',
        reuseExistingServer: true,
        timeout: 120_000,
    },
});

# 3) First test
import { test, expect } from '@playwright/test';

test('sign-up flow', async ({ page }) => {
    await page.goto('/signup');
    await page.getByLabel('Email').fill('mara@example.com');
    await page.getByLabel('Password').fill('correct-horse-battery-staple');
    await page.getByRole('button', { name: 'Create account' }).click();
    await expect(page.getByText('Welcome')).toBeVisible();
});

# 4) Selectors — prefer roles + accessible names + test ids
await page.getByRole('button', { name: 'Save' });
await page.getByLabel('Email');
await page.getByPlaceholder('Search…');
await page.getByText('Welcome', { exact: true });
await page.getByTestId('user-menu');

# Avoid:  page.locator('div > div:nth-child(2) > button.foo')   // brittle

# 5) Web-first assertions — auto-retry until pass or timeout
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByRole('list')).toHaveCount(5);
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page.getByRole('button')).toBeEnabled();

# 6) Fixtures — share setup, log in once per test
import { test as base, expect } from '@playwright/test';

type Fixtures = { authedPage: import('@playwright/test').Page };

export const test = base.extend<Fixtures>({
    authedPage: async ({ page }, use) => {
        await page.goto('/login');
        await page.getByLabel('Email').fill('mara@example.com');
        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);
    },
});

test('only logged-in users see settings', async ({ authedPage }) => {
    await authedPage.goto('/settings');
    await expect(authedPage.getByRole('heading', { name: 'Settings' })).toBeVisible();
});

# 7) Storage state — log in once, reuse session across tests
# global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
export default async function (config: FullConfig) {
    const browser = await chromium.launch();
    const page = await browser.newPage();
    await page.goto(`${config.projects[0].use.baseURL}/login`);
    await page.getByLabel('Email').fill('mara@example.com');
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.context().storageState({ path: 'storageState.json' });
    await browser.close();
}

# config
use: { storageState: 'storageState.json' }

# 8) Network mocking
await page.route('**/api/users/42', async (route) => {
    await route.fulfill({
        contentType: 'application/json',
        body: JSON.stringify({ id: 42, name: 'Mocked' }),
    });
});

await page.goto('/users/42');
await expect(page.getByText('Mocked')).toBeVisible();

# 9) Visual regression
await expect(page).toHaveScreenshot('home.png', { maxDiffPixels: 100 });
# First run creates the baseline. Subsequent runs compare; failures attach diff images.

# 10) CI integration — GitHub Actions
name: e2e
on: [push, pull_request]
jobs:
    test:
        timeout-minutes: 30
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: '20', cache: 'npm' }
            - run: npm ci
            - run: npx playwright install --with-deps
            - run: npm run build
            - run: npx playwright test
              env:
                  E2E_URL: https://staging.example.com
                  TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
            - if: always()
              uses: actions/upload-artifact@v4
              with:
                  name: playwright-report
                  path: playwright-report/
                  retention-days: 7

# 11) Sharding — split the suite across runners
strategy:
    fail-fast: false
    matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
steps:
    - run: npx playwright test --shard=${{ matrix.shard }}
# Cuts 30-minute runs to 8-10. Merge reports with @playwright/test's blob reporter.

# 12) Trace viewer + flake debugging
npx playwright show-trace test-results/.../trace.zip
# Frame-by-frame replay with network log, console, screenshots — debug flakes without re-running.

# 13) Stable selectors — add data-testid where text isn't reliable
<button data-testid="cart-checkout">Continue</button>
# Tests reference: page.getByTestId('cart-checkout')

# 14) Tagging tests for selective runs
test('@smoke landing page renders', async ({ page }) => { /* … */ });
test('@critical checkout completes', async ({ page }) => { /* … */ });

npx playwright test --grep '@smoke'

# 15) Auth strategies
# • Storage state (above) — sign in once, reuse cookies across many tests
# • API-based session creation — POST to /api/login, save cookies via context.addCookies
# • Magic-link flows — use a mailbox API (mailosaur, mailpit) to fetch the email + extract link
# • SSO providers — usually shareable storageState across the org; skip OAuth dance in CI

# 16) Test data — isolate by tenant or randomise
const email = `e2e+${Date.now()}@example.com`;
await createUser({ email });
# After the test, clean up — or use ephemeral test accounts created by CI fixtures.

# 17) Parallelism + test independence
# Every test must work with sister tests running concurrently.
# Common bugs that break parallelism:
#   • Shared user in DB → tests overwrite each other
#   • Hard-coded entity IDs
#   • Global counters not reset between tests
# Use unique data per test; tag with a run-id to scope cleanup.

# 18) Headed vs headless
# • CI:    headless (fast, no display)
# • Local: --headed --debug for stepping through
# • Trace: always on in CI; replay locally with show-trace

# 19) Visual + accessibility audits
# Pair Playwright with axe-core:
npm i -D @axe-core/playwright
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);

# 20) Common bugs
# • Tests pass locally, fail in CI → usually viewport size, locale, or slow network
# • Selectors break after refactor → switch to roles or data-testid
# • Flakes from race conditions → use web-first assertions, not setTimeout
# • CI runs forever on one stuck test → set test.setTimeout per test + per-action timeouts
# • Authenticated tests trip the rate limiter → cache sessions; whitelist test IPs
# • Storage state stale → rebuild on each CI run, don't commit to git
# • Screenshots don't match across OS → run visual tests in a Docker image for consistent rendering

Why it matters

Run Playwright in CI with retries, traces on failure, and sharding across 4-8 runners so the suite stays under 10 minutes. Use roles and accessible names for selectors, save signed-in storageState.json so most tests skip login, and treat flaky tests as bugs — web-first assertions plus the trace viewer almost always pinpoint a real race.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
- name: Playwright
  run: |
      npx playwright install --with-deps chromium
      npm run test:e2e
- if: failure()
  uses: actions/upload-artifact@v4
  with: { name: traces, path: test-results/ }
Try it Yourself »

Discussion

Loading…