Skip to content

Playwright Setup

How to configure Playwright, run tests locally, and understand the global authentication setup.

Playwright is installed as a dev dependency in platform/ui. The configuration file at platform/ui/playwright.config.ts defines test discovery, timeouts, retry behaviour, and the two test projects (authenticated and unauthenticated).


Configuration File

The key settings in playwright.config.ts:

// platform/ui/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e/specs',
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,

  use: {
    baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    {
      name: 'authenticated',
      use: {
        ...devices['Desktop Chrome'],
        storageState: '.auth/admin.json',
      },
    },
    {
      name: 'unauthenticated',
      use: {
        ...devices['Desktop Chrome'],
        storageState: { cookies: [], origins: [] },
      },
      testMatch: /.*\/(login|register|public)\..*/,
    },
  ],

  globalSetup: './e2e/global-setup.ts',
});

Notable settings:

  • retries: 2 in CI — flaky tests are retried twice before being marked as failed.
  • workers: 1 in CI — tests run sequentially to avoid state conflicts on the shared DEV environment.
  • trace: 'on-first-retry' — Playwright captures a trace file on the first retry, which you can inspect with npx playwright show-trace.
  • screenshot and video are captured only on failure and retained as pipeline artifacts.

Global Setup

e2e/global-setup.ts runs once before any test in the suite. It authenticates against the backend API and saves the browser storage state to .auth/admin.json.

// e2e/global-setup.ts
import { chromium, type FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
  const username = process.env.E2E_ADMIN_USERNAME || '';
  const password = process.env.E2E_ADMIN_PASSWORD || '';

  // Authenticate directly against the API — no UI login flow
  const response = await fetch(`${baseURL}/api/v1/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  });

  if (!response.ok) {
    throw new Error(`Auth failed: ${response.status} ${await response.text()}`);
  }

  const data = await response.json();
  const { access_token, refresh_token, user } = data;

  // Build the Zustand persist storage entry that the UI expects in localStorage
  const storageState = {
    cookies: [],
    origins: [
      {
        origin: baseURL,
        localStorage: [
          {
            name: 'auth-storage',
            value: JSON.stringify({
              state: {
                accessToken: access_token,
                refreshToken: refresh_token,
                user,
                isAuthenticated: true,
              },
              version: 0,
            }),
          },
        ],
      },
    ],
  };

  // Save state for reuse by the authenticated test project
  const browser = await chromium.launch();
  const context = await browser.newContext();
  await context.addInitScript(() => {});
  await context.storageState({ path: '.auth/admin.json' });

  // Write the constructed state directly
  const fs = await import('fs/promises');
  await fs.writeFile('.auth/admin.json', JSON.stringify(storageState, null, 2));
  await browser.close();
}

export default globalSetup;

Why bypass the login UI?

Using the API directly is faster and more reliable than driving the login form. The form flow tests itself in e2e/specs/auth/login.spec.ts. For all other tests, the goal is to start already authenticated — not to re-test the login page.


Environment Variables

Variable Default Purpose
PLAYWRIGHT_BASE_URL http://localhost:3000 Target application URL for all tests
E2E_ADMIN_USERNAME Username for the admin account used in global setup
E2E_ADMIN_PASSWORD Password for the admin account used in global setup

For local runs, set these in a .env.test file or export them in your shell before running the test command. In CI, they are fetched from Vault at job start (see CI Integration).


Running Tests Locally

Command Mode Description
npm run e2e Headless Runs all tests without a visible browser (same as CI)
npm run e2e:headed Headed Browser window is visible while tests run
npm run e2e:ui Interactive UI Playwright's graphical test runner — run, filter, and debug tests interactively
npm run e2e:codegen Code generator Opens a browser and records your actions to generate test code

All commands must be run from the platform/ui directory.

cd platform/ui

# Set credentials for local run
export E2E_ADMIN_USERNAME=admin@localhost
export E2E_ADMIN_PASSWORD=changeme

# Run all tests headlessly
npm run e2e

# Run a single spec file
npx playwright test e2e/specs/auth/login.spec.ts

# Run tests matching a name pattern
npx playwright test --grep "should display workflow list"

Use npm run e2e:codegen to scaffold new tests quickly.

codegen opens a real browser and records your clicks and typing. It generates the corresponding Playwright actions as you interact with the page, giving you a starting point that you can then refactor into a proper page object and spec.


Installing Browsers

Playwright manages its own browser binaries. If you're setting up a new machine or the browsers are out of date:

cd platform/ui
npx playwright install chromium

In CI, the Playwright Docker image (nexus.shared.cwiq.io:8444/playwright:v1.58.2-noble) already includes the required browser binaries. You do not need to run playwright install in CI pipelines.


The .auth/ Directory

The .auth/ directory is created by global-setup.ts and contains saved browser storage state. It is gitignored — never commit it.

platform/ui/
└── .auth/
    └── admin.json    # Saved localStorage state with auth tokens — gitignored

.auth/admin.json contains auth tokens.

This file is equivalent to a logged-in browser session. It is created fresh at the start of every test run. Never commit it to the repository.