Skip to content

Playwright Visual QA

Playwright visual regression is the primary mechanism for catching unintended UI changes before they reach customers. It captures screenshots of key UI states and compares them against approved baselines.

Why Visual Regression Matters

TypeScript compiles, tests pass, and the UI still looks broken. Visual regression exists because:

  • CSS changes have side effects across the component tree that tests cannot see
  • Font loading, animation states, and layout shifts are invisible to functional tests
  • The GOVERN Living Visualization doctrine requires specific visual properties (energy layers, ambient motion, orb-first interface) that cannot be unit tested

The Playwright visual suite is the only mechanism that can verify these properties automatically.

Setup

expressiveCode.terminalWindowFallbackTitle
# Install Playwright and browsers (run once per machine)
cd packages/govern-internal-docs
npx playwright install --with-deps chromium
# Or from the monorepo root
pnpm add -D @playwright/test
npx playwright install chromium

playwright.config.ts

import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4321',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium-desktop',
use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } },
},
{
name: 'chromium-mobile',
use: { ...devices['Pixel 5'] },
},
],
snapshotDir: './e2e/snapshots',
updateSnapshots: 'missing',
});

Taking and Comparing Screenshots

Writing a visual test

import { test, expect } from '@playwright/test';
test('dashboard home renders correctly', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Wait for ambient animation to reach steady state
await page.waitForTimeout(2000);
await expect(page).toHaveScreenshot('dashboard-home.png', {
maxDiffPixels: 100,
threshold: 0.02,
});
});
test('assessment panel with active probe', async ({ page }) => {
await page.goto('/assessments/test-assessment-id');
await page.waitForSelector('[data-testid="probe-status"]');
await expect(
page.locator('[data-testid="assessment-panel"]')
).toHaveScreenshot('assessment-panel-active.png');
});

Updating baselines

When a UI change is intentional, update the baseline screenshots:

expressiveCode.terminalWindowFallbackTitle
# Update all baselines
npx playwright test --update-snapshots
# Update a specific test file
npx playwright test e2e/visual/dashboard.spec.ts --update-snapshots

Always review updated baselines before committing. The Testing Dashboard shows a side-by-side diff of old vs new baseline.

Screenshot Inventory

These are the canonical screenshots maintained as visual regression baselines:

Customer Dashboard

ScreenshotPageState
dashboard-home.png/Idle, ambient animation at 2s
assessments-list.png/assessments3 assessments, mixed status
assessment-detail.png/assessments/:idRunning probe, live telemetry
report-preview.png/reports/:idPDF preview, all sections loaded
settings-org.png/settings/orgOrg settings form

Internal Dashboard

ScreenshotPageState
internal-home.png/All widgets loaded, 24h view
pipeline-monitor.png/pipelineActive pipeline, recent events
build-events.png/build-eventsLast 20 events, skill graph
cost-governor.png/costCurrent period, spend trend
deploy-watchdog.png/deploysLast 5 deploys, health badges

CI Integration

Visual regression runs in GitHub Actions on every PR that touches a UI package:

.github/workflows/visual-regression.yml
name: Visual Regression
on:
pull_request:
paths:
- 'packages/dashboard/**'
- 'packages/govern-dashboard/**'
- 'packages/govern-app/**'
jobs:
visual-qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: pnpm install
- run: npx playwright install --with-deps chromium
- run: pnpm build
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/

Reviewing Failures

When visual regression fails, the Testing Dashboard shows:

  1. Expected — the approved baseline screenshot
  2. Actual — the screenshot from the failing test run
  3. Diff — pixel-level diff with changed areas highlighted in red
  4. Threshold info — how far the diff exceeds the allowed threshold

Common failure causes

Failure typeLikely causeResolution
Entire page shiftedViewport size mismatchCheck playwright.config.ts viewport settings
Animation mid-framewaitForTimeout too shortIncrease wait before screenshot
Font rendering differenceDifferent OS font renderingRun tests in Docker to standardize
Data-driven text changedTest database seeding changedReset test database seeds
Intentional redesignUI deliberately changedReview + approve, then --update-snapshots

Living Visualization Checks

For GOVERN UI surfaces that implement the Living Visualization doctrine, visual tests include specific checks:

test('energy layers are visible at idle', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(3000); // Let ambient animation stabilize
// L1 — base energy layer
const l1 = page.locator('[data-layer="energy-l1"]');
await expect(l1).toBeVisible();
// L2 — mid energy layer
const l2 = page.locator('[data-layer="energy-l2"]');
await expect(l2).toBeVisible();
// L3 — surface energy layer
const l3 = page.locator('[data-layer="energy-l3"]');
await expect(l3).toBeVisible();
// Full page screenshot for visual baseline
await expect(page).toHaveScreenshot('idle-energy-layers.png');
});
test('orb is the dominant interface element', async ({ page }) => {
await page.goto('/');
const orb = page.locator('[data-testid="civilization-orb"]');
await expect(orb).toBeVisible();
// Orb bounding box should occupy center of viewport
const orbBox = await orb.boundingBox();
const viewportSize = page.viewportSize();
expect(orbBox?.x).toBeGreaterThan(viewportSize!.width * 0.3);
expect(orbBox?.x).toBeLessThan(viewportSize!.width * 0.7);
});

Snapshot Storage

Baseline snapshots are committed to the repository in e2e/snapshots/. They are versioned alongside the code they test.

  • Do not use .gitignore to exclude snapshot files
  • Do review snapshot changes in PRs the same way you review code changes
  • Do delete snapshots for removed UI components to keep the baseline lean