GenerateSaaS

End-to-end tests

The Playwright harness that owns the server, its preflight checks, the offline vendor fakes, the live gate, and the loop knobs.

apps/web/e2e/ drives the real app in Chromium. The harness owns the whole run: services, build, server, throwaway database, and the teardown of all of it.

What a run does

Preflight - one check per dependency (Postgres, Redis, Mailpit, SMTP, Inngest, MinIO, Chromium, the port). Each failure is one line naming pnpm infra; nothing is built until all pass.
Build - next build into a per-suite directory, so a run never touches the one pnpm dev uses.
Serve - the server starts in its own process group and its pid is recorded, so a killed run cannot strand it on the port.
Run - specs execute with retries: 0, retaining a trace for every failure.
Teardown - server, fakes, then the database, in that order.

Offline by construction

Would need the internetWhat the suite uses instead
OpenRouterA local fake: fixed model catalog, deterministic streamed completions, exact prices
StripeA local fake serving the endpoints the billing plugin calls
Umami, Chatwoot, TurnstileAborted at the browser. Any other host fails the spec, naming the URL
The desktop release feedThe same fake origin

Ports and services

FactValue
Web suite port4100 + (checkout hash % 900); WEB_E2E_ORIGIN overrides
Desktop suite port5100 + (the same hash % 900); DESKTOP_E2E_ORIGIN overrides
Build directoryapps/web/.next-e2e-web and apps/web/.next-e2e-desktop
ServicesPostgres, Redis, Mailpit, Inngest, MinIO - each on its pnpm infra default port

Each checkout gets its own port, database, Redis key prefix and Inngest app id, so two worktrees can run at once.

The harness refuses to start against a cache that is not on this machine. It writes to the cache and its teardown deletes every key under its own prefix, so a REDIS_URL or UPSTASH_REDIS_REST_URL pointing at a live one stops the run until you set the knob below.

Where a failure's evidence lives

Playwright empties test-results/ at the start of every run - including the run you start to reproduce the failure. Read the copy the harness keeps instead.

WantLook in
Trace, screenshot, video, error-context.mdapps/web/e2e-logs/<suite>-<run id>/
The server's own log for that runapps/web/e2e-logs/<suite>-<run id>.log

The run prints both paths as it tears down. Older runs are pruned, so copy anything you need to keep.

The live gate

*.live.spec.ts files run only under pnpm e2e:live. The harness first probes each vendor key and coding CLI with one real minimal turn; anything that cannot complete one prints not run: <item> — <why> and its specs never register. The run exits 0 when nothing is available.

Live runs reach real vendors and cost real money. pnpm e2e:ci does neither. See Live vendor tests for the keys, the cost, and how the lane gates.

Knobs

Set the bottom three only when your services are not the ones pnpm infra starts. GENERATESAAS_E2E_EXTRA_SERVER_ENV is the one extension point for a project whose own runtime schema requires a key this harness never heard of - it only ADDS, and naming a key the harness already sets fails the run by name.

VariableEffect
GENERATESAAS_E2E_DEV_SERVER=1Run against next dev - nothing is cached, so nothing can be stale
GENERATESAAS_E2E_LIVE=1Treat the run as live, for IDE and UI-mode sessions
GENERATESAAS_E2E_EXTRA_SERVER_ENVExtra server environment as a flat JSON object: {"CLI_SHARED_SECRET":"e2e-secret"}
WEB_E2E_ORIGIN / DESKTOP_E2E_ORIGINMove a suite's origin, for two checkouts that hash to one port
SMTP_HOST=::1Mailpit binds SMTP on IPv6 only, so 127.0.0.1:1025 is refused
STORAGE_ACCESS_KEY_ID / _SECRET_ACCESS_KEYYour MinIO is not on the compose file's minioadmin pair
GENERATESAAS_BATTERY_DATABASE_URLThe admin connection CREATE DATABASE runs against, when yours is not postgres:postgres@127.0.0.1:5432
GENERATESAAS_E2E_ALLOW_REMOTE_CACHE=1Accept a cache outside this machine, teardown's prefix delete included

CI=1 pins one worker. Locally Playwright takes one per core, and all of them drive the same server - past about four they starve it and unrelated specs time out. Pass --workers=4.

pnpm e2e:ci covers the web app only. The desktop app has its own Playwright harness, with its own command, its own port and its own build directory - see desktop end-to-end tests.

Adding a spec

Create apps/web/e2e/<journey>.spec.ts importing { expect, test } from "@playwright/test".
Prefer stable selectors - ids, ARIA roles, visible text - over CSS classes.
Create contexts with newE2eContext(browser) from e2e/fixtures, never browser.newContext - that helper installs the origin block.
Gate on config with a plain if, never test.skip - see Writing tests.
Wrap a hydration-dependent click-then-assert in expect(...).toPass(), so a pre-hydration no-op click retries.

On this page