End-to-end testing
Run and extend the Playwright browser suite against an isolated local product stack.
End-to-end testing
Playwright verifies the browser-to-API boundary that component and server tests cannot: actual routes, cookies, redirects, form interaction, browser state, and cross-origin policy. The suite is run in CI on Chromium after database-backed tests.
The suite is not a substitute for real provider checkout, mailbox, cloud, backup, or production browser verification. Those remain release smoke tests under Go live.
Run the suite locally
Install Chromium once for the pinned Playwright version:
pnpm exec playwright install chromiumStart Postgres, create a disposable database, and run the root command with that exact database:
docker compose up -d postgres
createdb app_e2e
DATABASE_URL='postgresql://app:app@localhost:5432/app_e2e?sslmode=disable' \
pnpm test:e2eThe Playwright configuration migrates that database, starts the API on 127.0.0.1:8000, starts the
web app on 127.0.0.1:5173, and uses test-only local origins. It disables external email and Slack
delivery. The suite creates test users and data, so never point it at staging, production, or a
shared developer database.
Remove the disposable database when finished:
dropdb --if-exists --force app_e2eWhat the current suite proves
e2e/auth.spec.ts covers public and protected authentication behavior. e2e/product-smoke.spec.ts
covers the authenticated application shell and key browser boundaries. CI installs Chromium and runs
the same pnpm test:e2e command with its disposable PostgreSQL service.
Add browser coverage when a change affects:
- login, sign-out, verification, reset, magic link, OAuth, session expiry, cookies, or redirects;
- a new protected page, form, user-visible mutation, loading/empty/error state, or route fallback;
- billing checkout entry points, provider return handling, entitlement presentation, or portal entry;
- upload selection/confirmation, PWA update/offline behavior, or an origin-sensitive integration.
Keep provider-side payment completion, external email delivery, DNS, and cloud ingress in their provider or deployment smoke instead of putting real credentials into Playwright.
Add a browser test
Create e2e/<feature>.spec.ts. Start from user-visible behavior, use accessible role/label queries,
and make the result observable. Prefer a short independent flow over a sequence coupled to another
test's data.
import {expect, test} from '@playwright/test'
test('a signed-in user can create a saved link', async ({page}) => {
await page.goto('/saved-links')
await page.getByRole('button', {name: 'Add saved link'}).click()
await page.getByLabel('URL').fill('https://example.com')
await page.getByLabel('Label').fill('Example')
await page.getByRole('button', {name: 'Save link'}).click()
await expect(page.getByRole('link', {name: 'Example'})).toBeVisible()
})The example assumes its domain, route, labels, and browser flow have already been built. Do not add test-only product endpoints or bypass Better Auth to make a test convenient. For a durable feature, first follow the worked feature recipe and the domain golden path.
Failure evidence
Playwright retains traces, screenshots, and video on failure. The API process output is written to
.e2e/server.log. Inspect the first browser assertion, browser console/network evidence, and server
request ID before changing selectors, environment, auth, or application code.
When a browser test fails only in CI, record the exact command, database name, Playwright version, trace, and redacted server log. Do not paste cookies, passwords, auth links, provider payloads, or environment files into an issue or support request.
Keep the suite useful
- Test the product outcome, not CSS class names, implementation state, or a timing guess.
- Use deterministic fixtures and independently created users for ownership boundaries.
- Assert unauthenticated redirects, invalid input, failures, and success for a changed critical flow.
- Keep browser data isolated to the named disposable database.
- Run Testing as well: browser E2E complements contracts, real Postgres integration, server tests, and the production build.