Files
pad/web/playwright.config.ts
T
xarmian c8601a2031 test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689) (#225)
* test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689)

Option A of TASK-689: land the test infrastructure and a minimal smoke
test on both mobile and desktop viewports. Broader flow coverage (board
view drag, item detail, comments, mobile hamburger, BottomSheet
regression guard) is tracked as TASK-733.

Infrastructure
--------------
- web/playwright.config.ts: two projects (desktop-chromium, mobile-
  chromium via Pixel 7), reporter list+html, trace/video/screenshot
  retained on failure, webServer that wipes + recreates the data dir
  then runs the pad binary. Paths anchored to the config file's
  directory so runs are cwd-invariant.
- web/e2e/global-setup.ts: bootstraps admin via POST /auth/bootstrap,
  logs in, creates the e2e workspace, mints a user-scoped API token,
  and persists the token + resolved admin username to fixture.json.
- web/e2e/fixtures.ts: extends base test so every BrowserContext
  automatically gets Authorization: Bearer <token>. Uses a token
  rather than a session cookie because sessions are User-Agent bound
  in middleware_auth.go and a node-minted session would be rejected
  by a Chromium UA.

Tests
-----
- web/e2e/dashboard.spec.ts: a logged-in user lands on the seeded
  workspace, no login form is rendered, and the workspace name
  appears on the page. Runs in both project viewports.

CI
--
- New `e2e` job in .github/workflows/ci.yml: builds web UI + binary,
  installs Playwright chromium with OS deps, runs the suite, and
  uploads the HTML report as an artifact on failure. Timeout capped
  at 10 minutes (suite itself runs in ~4s today).

Local run (in mcr.microsoft.com/playwright:v1.59.1-noble): 2 passed
in 4.1s.

Parent: PLAN-644.
Follow-up: TASK-733 for broader flow coverage (Option B in the
original ship plan).

* fix(e2e): persist server-returned workspace slug instead of the constant (TASK-689)

Addresses Codex P2 on PR #225. When Playwright's `reuseExistingServer:
true` (local dev), a re-run of globalSetup hits `POST /api/v1/workspaces`
against a DB that already has `e2e`. The server uniquifies the slug
(`e2e` → `e2e-2` → …) and returns the uniquified value, but the old
code wrote `WORKSPACE_SLUG` (the constant) to fixture.json. Tests
then navigated to /e2e-admin/e2e — which might still exist from a
previous run with stale state — instead of /e2e-admin/e2e-2, missing
regressions in freshly-seeded content.

Fix: read `slug` back from the workspace-create response and use that
when writing fixture.json. Local re-runs now always point at the
workspace this run actually created.

Parent: PLAN-644.

* fix(e2e): cross-platform webServer bootstrap via Node wrapper (TASK-689)

Addresses Codex P2 on PR #225: `rm -rf && mkdir -p && pad server start`
in webServer.command is POSIX-only. Windows contributors on cmd.exe or
PowerShell can't run `npm run test:e2e` at all — the e2e suite becomes
Linux/macOS-only, defeating the "CI parity" goal.

Fix: extract the wipe-and-exec logic into web/e2e/run-pad.mjs. Node's
fs.rmSync / mkdirSync / child_process.spawn are uniform across
platforms, and the wrapper forwards SIGTERM/SIGINT so Playwright's
teardown still cleanly kills the child on suite exit.

Local re-run in mcr.microsoft.com/playwright:v1.59.1-noble: 2 passed.

Parent: PLAN-644.
2026-04-22 22:35:08 -04:00

105 lines
3.7 KiB
TypeScript

import { defineConfig, devices } from '@playwright/test';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// Anchor all test paths to the config file's directory so runs are
// invariant under the caller's cwd (Playwright's default resolves
// relative paths against cwd, which differs between `npx playwright
// test` and CI invocations).
const HERE = dirname(fileURLToPath(import.meta.url));
/**
* Playwright config for Pad end-to-end smoke tests.
*
* The suite exercises the real Pad binary (built from this repo) serving
* its embedded web UI. That's the same shape a user gets from
* `pad server start`, so regressions that only show up once the web
* assets are embedded are caught here rather than in svelte-check.
*
* See e2e/global-setup.ts for how admin bootstrap + workspace seeding
* happen before any test runs.
*/
const E2E_PORT = Number(process.env.PAD_E2E_PORT ?? 17800);
const E2E_HOST = process.env.PAD_E2E_HOST ?? '127.0.0.1';
const BASE_URL = `http://${E2E_HOST}:${E2E_PORT}`;
// Build a private data dir for this run so the test instance never
// touches the developer's real ~/.pad or another CI run's artifacts.
const DATA_DIR = process.env.PAD_E2E_DATA_DIR ?? resolve(HERE, '..', '.pad-e2e');
// Pad binary relative to the repo root. `make build` / `make build-go`
// writes ./pad in the repo root; CI builds it explicitly before the
// e2e job runs.
const PAD_BINARY = process.env.PAD_BINARY ?? resolve(HERE, '..', 'pad');
export default defineConfig({
testDir: './e2e',
timeout: 30_000,
expect: { timeout: 5_000 },
// Playwright's reporter list kept minimal; CI uses the list reporter
// and uploads the HTML report as an artifact on failure.
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
// Run tests in parallel but cap workers on CI to keep the suite under
// the 2-minute target quoted in TASK-689.
fullyParallel: true,
workers: process.env.CI ? 2 : undefined,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
use: {
baseURL: BASE_URL,
trace: 'retain-on-failure',
video: 'retain-on-failure',
screenshot: 'only-on-failure'
// NOTE: auth is applied per-test via the fixture in e2e/fixtures.ts
// rather than config.use.storageState. The config approach proved
// flaky: Playwright resolves `storageState` against disk at
// project-setup time (before globalSetup runs), so the file written
// by globalSetup wasn't picked up. Passing cookies through an
// explicit `test.extend` fixture sidesteps that ordering entirely.
},
globalSetup: './e2e/global-setup.ts',
projects: [
{
name: 'desktop-chromium',
use: { ...devices['Desktop Chrome'] }
},
{
// Mobile viewport — triggers the mobile BottomSheet branch
// (CONVE-639: isMobile ≤ 639.98px on WorkspaceSwitcher).
// Pixel 7 ships with a Chromium defaultBrowserType, so we keep
// one installed browser for both projects and avoid downloading
// WebKit in CI.
name: 'mobile-chromium',
use: { ...devices['Pixel 7'] }
}
],
webServer: {
// Wipe the data dir BEFORE the server starts, so migrations run
// against an empty SQLite file every run. Doing this in globalSetup
// would race with the already-running server — it has the DB file
// open by then. The wrapper script is Node-based so it works on
// Windows (cmd/PowerShell), not just POSIX shells.
command: `node ${resolve(HERE, 'e2e', 'run-pad.mjs')}`,
url: `${BASE_URL}/api/v1/health`,
timeout: 30_000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
env: {
PAD_BINARY,
PAD_HOST: E2E_HOST,
PAD_PORT: String(E2E_PORT),
PAD_DATA_DIR: DATA_DIR,
PAD_LOG_LEVEL: 'warn'
}
}
});