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: 2in CI — flaky tests are retried twice before being marked as failed.workers: 1in 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 withnpx playwright show-trace.screenshotandvideoare 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:
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.
.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.
Related Documentation¶
- E2E Testing Overview — Architecture, test projects, and how authentication works
- Page Object Model — How to structure page objects and spec files
- CI Integration — How the E2E job is configured in the pipeline
- ReportPortal — How test results are reported