Basilic
Testing

E2E Testing

Playwright E2E for Next.js and Fastify. Frontend apps have no separate unit suite.

Frontend apps have no separate unit suite. Two Playwright suites: Web (apps/web) for the product UI, API (apps/api) for Scalar login at /reference. Both use @test.ai addresses when ALLOW_TEST=true.

The Scalar page loads @scalar/api-reference from jsDelivr (pinned version in reference/template.ts). E2E depends on that CDN for the login button script.

Install Chromium for both suites with pnpm setup:playwright (or pnpm setup) if browsers are missing.

Root pnpm qa builds, then ends with pnpm test:e2e (SKIP_BUILD=1, scripts/run-e2e.mjs). That script kills processes on ports 3000 and 3001 unless SKIP_KILL_PORTS=1. Shared spawn env lives in scripts/e2e-local-shared.mjs.

Commands

CommandDescription
pnpm test:e2e (root)Fastify E2E, then Next E2E (spawns servers)
pnpm --filter @repo/web test:e2e:localBuild (unless SKIP_BUILD=1), spawn API + Next, run web E2E
pnpm --filter @repo/api test:e2e:localSpawn API (RATE_LIMIT_MAX=10000), run Scalar login E2E — no ANTHROPIC_API_KEY required
pnpm test:e2e:ui / test:e2e:debugPlaywright UI or debug (in the app)

Pass URLs with --app / --api, or PLAYWRIGHT_APP_URL / PLAYWRIGHT_API_URL. Defaults: http://localhost:3000 and http://localhost:3001.

pnpm test:e2e --app=https://my-app.vercel.app --api=https://my-api.vercel.app

Session model

Use one worker (PGlite cannot take concurrent writers). The setup project logs in as test@test.ai once and writes test-results/.auth/user.json. chromium, security, and chat reuse that file via project storageState. auth, passkey-login, and logout tests use empty storage so they never revoke the shared session.

Import page from @playwright/test in all specs. Navigate to the target route in each authed test (page.goto('/settings/...')).

import { expect, test } from '@playwright/test'

test('shows dashboard when authenticated', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('text=Signed In')).toBeVisible()
})

Chat E2E needs a real ANTHROPIC_API_KEY (in-app Claude Haiku 4.5 by default). Playwright omits the chat project when hasRealAnthropicKey() is false (empty, sk-ant-xxx, sk-ant-dummy*), so fork CI without org secrets still runs auth and dashboard E2E. When the key is set, chat-assistant.spec.ts skips only quota or credit errors; upstream 5xx and network failures fail the test run. API Scalar E2E (pnpm --filter @repo/api test:e2e:local) does not call AI and does not require Anthropic. API remote tests (ai.spec.ts) use test/utils/ai-remote.ts: skip 402 always; skip 502/503/504 only with placeholder/missing key — real key fails on 502. See AI Architecture.

Test helpers

ALLOW_TEST=true stores plain tokens in verification.token_plain for @test.ai addresses.

EndpointUse
GET /test/verification/last?type=magic_link|change_email&email=Email-scoped tokens for web E2E
GET /test/magic-link/last?email=Magic link token for Scalar E2E (email required, @test.ai only)
GET /test/totp/currentBearer required; in-progress TOTP setup code

Local spawn scripts set ALLOW_TEST. The API process exits if ALLOW_TEST is true in production.

Playwright projects (order)

  1. public — legal pages, login smoke, robots/sitemap
  2. auth01-callbacks06-logout (empty storage; depends on public; file-name order)
  3. setup — writes user.json (depends on auth)
  4. chromium — dashboard smokes, 404 (storageState)
  5. security — TOTP, API keys, passkeys CRUD (storageState; never logout)
  6. passkey-login — passkey sign-in with e2e-passkey@test.ai (empty storage; depends on setup)
  7. chat — assistant (storageState; mobile viewport; last)

Dedicated emails: test@test.ai (shared), e2e-passkey@test.ai, e2e-email@test.ai (change-email).

Web specs

SpecProjectCoverage
public.spec.tspublicPrivacy, terms, login, robots/sitemap
01-callbacks.spec.tsauthOAuth/passkey/web3 missing-param errors
02-magic-link-auth.spec.tsauthLogin, token errors, JWT session
03-proxy-gate.spec.tsauthUnauthenticated redirects
04-update-tokens.spec.tsauthCookie adapter CSRF/origin
05-change-email.spec.tsauthIn-page email change verify
06-logout.spec.tsauthHeader sign-out revokes sid
auth.setup.tssetupWrites shared storageState
dashboard.spec.tschromiumNews, markets, profile, 404
security/authenticator.spec.tssecurityTOTP setup/unlink
security/api-keys.spec.tssecurityAPI keys CRUD
security/passkeys.spec.tssecurityPasskeys add/remove
passkey-auth.spec.tspasskey-loginPasskey sign-in flow
chat-assistant.spec.tschatAssistant Who am I?

CI and mobile

Path-filtered PR workflows: api-e2e.yml, web-e2e.yml, packages-test.yml. See GitHub Actions.

Expo uses Maestro (apps/mobile/.maestro/flows/home.yml). CI Maestro is deferred; see Mobile CI/CD.

Vercel protection (manual)

For protected previews: enable Protection Bypass for Automation on both projects with the same secret, set VERCEL_AUTOMATION_BYPASS_SECRET, and allowlist OPTIONS / or /auth on the API project.

Troubleshooting

Ports in use: bash scripts/kill-test-servers.sh. Token extraction fails: confirm ALLOW_TEST=true and the correct @test.ai email query on /test/verification/last. Magic link 500 mid-suite: local/CI spawn sets RATE_LIMIT_MAX=10000 (default API limit is 100/min per IP). Passkey registration fails in CI: spawn scripts and web-e2e.yml set WEBAUTHN_RP_NAME=Test App when .env.test is absent.

On this page