Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    Playwright

    End-to-end tests in real browsers with accessible locators, auth handled once via saved storage state, network control for deterministic runs, and traces that make CI failures debuggable.

    Growingtended planted 2 min read
    On this page

    Playwright drives real Chromium, Firefox and WebKit browsers from tests. When I first picked it up, the hard part wasn’t writing tests. It was authentication. Corporate apps sit behind single sign-on, and an end-to-end test that has to click through an identity provider every run is slow and flaky, and sometimes blocked outright.

    Auth: log in once, reuse everywhere

    The standard answer now is a setup project that logs in once and saves the browser’s storage state. Every other test starts already authenticated.

    // auth.setup.ts: runs once before the suite
    import { test as setup } from "@playwright/test";
    setup("authenticate", async ({ page }) => {
      await page.goto("/login");
      await page.getByLabel("Email").fill(process.env.E2E_USER!);
      await page.getByLabel("Password").fill(process.env.E2E_PASS!);
      await page.getByRole("button", { name: "Sign in" }).click();
      await page.context().storageState({ path: "playwright/.auth/user.json" });
    });
    // playwright.config.ts
    projects: [
      { name: "setup", testMatch: /auth\.setup\.ts/ },
      { name: "chromium", use: { storageState: "playwright/.auth/user.json" }, dependencies: ["setup"] },
    ];

    For SSO that resists automation, use a test-only login route or dedicated test users in non-production environments, and keep one smoke test that exercises the real SSO flow. You rarely need to test the identity provider itself on every run.

    Habits that keep a suite healthy

    • Accessible locators. getByRole and getByLabel are resilient to markup changes and nudge you towards accessible UI. It’s the same philosophy as Testing Library, applied to a real browser.
    • Control the network when you need determinism. page.route() can fulfil requests with fixtures, so demo and smoke suites don’t depend on volatile backend data. Keep some tests against the real backend too.
    • Traces on retry. --trace on-first-retry captures a timeline with DOM snapshots, network and console logs. It turns “flaky in CI” into an actual root cause.
    • Shard and split. Run smoke tests on every pull request and the full suite on merge, sharded across machines.
    • Codegen for a starting point. It’s great for live demos and first drafts. Tidy up the unnecessary steps afterwards.

    Component testing too

    Playwright can also mount individual components in a real browser, which makes a useful middle ground between jsdom unit tests and full end-to-end journeys. Storybook stories make good fixtures for it (Storybook).

    In practice

    I championed Playwright for end-to-end testing across several teams in one department. Mocked-state suites ran in pipelines and doubled as reliable environments for client demos and user acceptance testing. The biggest unlock was organisational: agreeing dedicated test users and auth policies for test environments. Once that was sorted, the tests themselves were the easy part. Test against the environment you actually run (SAID: Train for the Production You Actually Run). Where it fits in the bigger picture: React Testing Strategy and A Testing Strategy Teams Actually Adopted.

    • #playwright
    • #e2e-testing
    • #testing
    • #ci

    ↪ Links to (4)