Cooperative email for PDS operators
README.md

E2E tests against staging UI #

Playwright + TypeScript browser tests that exercise the /account dashboard against the staging atmos-relay. Complements cmd/staging-probe (HTTP-level signup flow) by covering UI rendering and browser-only behavior.

What's tested #

  • dashboard.spec.ts — signed-in demo member sees their handle + domain on /account

More flows land in Phase 2 (sign-out, add-domain form, /enroll/auth start, /api/v1/me).

How auth works #

The tests use /admin/dev/mint-session (internal ref) to issue a short-lived JWT for did:plc:demo-demo-staging.example — the persistent demo member seeded on staging by /admin/dev/seed-demo. The endpoint refuses any DID that doesn't start with did:plc:demo-, so the test stack is structurally incapable of impersonating real members even if the admin token leaks.

The JWT is dropped onto the Playwright BrowserContext as the atmos_account_session cookie (Path=/account, matching the prod cookie attrs).

Running locally #

Staging is reachable only from the operator's private network (e.g. a Tailscale tailnet or VPN). Substitute your own staging URL for {STAGING_BASE_URL} below.

cd tests/e2e
npm ci
npx playwright install --with-deps chromium

export STAGING_BASE_URL=https://{STAGING_BASE_URL}
export STAGING_ADMIN_TOKEN=$(sops -d ../../infra/secrets/staging.yaml | grep admin_token | awk '{print $2}')

npm test

For interactive debugging:

npm run test:headed    # open a real browser window
npm run test:debug     # Playwright inspector
npm run report         # open the HTML report after a run

Running in CI #

.gitea/workflows/staging-e2e.yml triggers:

  • workflow_run after a successful Staging Probe run (gates expensive E2E behind the cheaper HTTP-level probe)
  • workflow_dispatch — manual button in the Gitea Actions UI

The workflow runs inside mcr.microsoft.com/playwright:v1.50.0-noble on the docker-build runner (the only combination known to work on Gitea — see memory gitea-playwright-ci.md). Tailscale is set up inline with the same --tun=userspace-networking + --outbound-http-proxy-listen pattern as staging-probe.yml.

Adding tests #

  • Keep tests independent: no shared state between spec files. The demo member is read-only common infrastructure; never mutate it.
  • For write-path tests (Phase 3+), seed a fresh member per test via /admin/dev/seed-demo with a unique domain, then /admin/dev/delete-member in afterEach.
  • Use Playwright's web-first assertions (await expect(locator).toBeVisible()) — they auto-retry within the timeout. Don't manually poll with await locator.isVisible().
  • Prefer role-based locators (page.getByRole('heading', ...)) over CSS selectors — they're resilient to template refactors.

Why staging-only, no localhost mode #

Mint-session is staging-only by three independent gates (env=staging, hostname check, marker file). The test stack therefore can't authenticate against a local dev relay; running locally still hits the real staging microvm. This is intentional — the value of E2E is verifying the deployed system, not a dev approximation of it.