Skip to main content

E2E testing

Constellation runs a Playwright end-to-end suite across all four apps — Catalog, Directory, Project Tracker, and Wiki. This page covers how it runs in CI, how to run it locally, and how to add a test for a new screen. Everything here is the human-facing version of the agent-oriented e2e-testing skill in .claude/skills/e2e-testing/.

The required tier: smoke specs

Every spec named *.smoke.spec.ts is a required merge gate — it runs in the E2E Smoke GitHub Actions job on every pull request, and the job must pass before merge. Everything else under an app's tests/e2e/ directory is advisory: useful locally, not enforced in CI (a broader nightly sweep is tracked separately).

The convention is deliberately narrow: one smoke spec per critical GUI path, kept fast (well under a minute) and self-contained, asserting that the primary authenticated surface actually renders rather than exercising every edge case.

How CI runs it

The E2E gate is split into four independent, change-scoped legs that fan into one stable required check named E2E Smoke. A detect-e2e-zones job looks at which paths a PR touched and decides which legs actually need to run — a Catalog-only change runs only the Catalog leg; a change to a shared package or the CI workflow itself runs all four; a docs- or spec-only PR runs none (the aggregator still reports green, so the gate never hangs waiting on a leg that was never going to run). Pushes to develop/main always run all four legs as an integration backstop.

Each leg builds its app with a production buildnext build followed by next start — rather than next dev, then runs every *.smoke.spec.ts for that app against the running server:

AppPortWhat it asserts
Project Tracker3002Saved-filter round-trip, parents view, tasks page
Directory + Project Tracker (coordinator pair)3001/3002Coordinator drawer, full-page view, artifact promotion (cross-zone)
Catalog3010Authenticated catalog home surface renders
Wiki3020Authenticated wiki shell renders (including the empty-wiki state)

Each leg has its own Postgres service and applies its own app's schema migrations, then boots the production server through the same warm-up scripts used previously — probing stays gated on the server actually logging that it's ready, and the leg restarts once if it gets stuck serving 404s for a route that should exist. On failure, the job uploads the raw server logs and the app's Playwright HTML report as CI artifacts so the failure is diagnosable without re-running.

Serving a production build requires the mock auth provider to remain reachable outside development mode. CI's serve and test steps set a narrowly-scoped, CI-only environment variable that a shared policy in @constellation-platform/auth-core verifies before allowing this — see that package's README for the full guard model. The variable is set only on those CI steps and must never appear in a real deployment's environment.

Wall-time improvement

Moving to change-scoped parallel legs and production builds cuts typical PR wall time from roughly 15–18 minutes (all four apps, sequentially, against next dev) down to about 4–6 minutes for a typical single-module PR — mostly the time for next build, often a Turbo remote-cache hit. A PR that touches a shared package or the workflow itself still runs all four legs, but now in parallel rather than sequentially, so the worst case is roughly the slowest single leg (about 6–10 minutes) instead of the sum of all four. Moving off next dev also eliminates the turbopack cold-compile flake class from the test path entirely — a Rust panic in turbo-tasks-backend that could previously kill a zone mid-run on completely unrelated code.

Running locally

From the repo root:

docker-compose -f docker-compose.dev.yml up -d # Postgres, MinIO
npx turbo run build --filter=@constellation-platform/* # build shared testing package (dist/)
cd apps/project-tracker && npm run db:setup # seeds the shared identity fixtures

The platform build is required once on a fresh checkout: each app's playwright.config.ts and its specs import from the shared @constellation-platform/testing package, which resolves to its dist/ output.

Then, from any app directory (apps/catalog, apps/directory, apps/project-tracker, apps/wiki):

npm run test:e2e

Playwright boots the app's dev server itself (via each app's playwright.config.ts) — there's no need to start npm run dev by hand first. If the app isn't Project Tracker, also run that app's own npm run db:migrate once so its schema exists.

Adding a test for a new screen

When a PR adds a new authenticated UI surface, add a *.smoke.spec.ts for its critical path (or extend an existing one). The convention:

  1. Authenticate programmatically. Every spec logs in via a shared helper that POSTs to the app's mock login route and stores the returned session cookie — no spec drives the login form directly. This keeps auth fast and removes an entire class of login-related flake.
  2. Prefer API assertions over DOM assertions where they prove the same thing — checking an authenticated API response is faster and more stable than waiting on a render. Reserve DOM assertions for what's genuinely visual: does the shell render at all, does a specific control appear.
  3. Use accessible locators (role, label) rather than CSS selectors. Where an element is ambiguous or dynamically generated, add a data-testid following the module's naming convention, rather than reaching for a brittle selector.
  4. Explore the real UI before writing the spec. Boot the app locally and interact with the live page to find the actual locators, rather than guessing from the component source — the DOM the browser renders is the source of truth for a locator, not the JSX.

Full detail, including the exact helper import path and the CI boot-script layout, lives in .claude/skills/e2e-testing/SKILL.md in the repository.

  • Request lifecycle — how one request flows through the layers a smoke spec exercises end-to-end.
  • Tenancy & RLS — the tenant-isolation rules the seeded test identities operate under.
  • Module references: Catalog, Wiki.