name: pinpoint-e2e description: E2E testing guide for PinPoint (Playwright, Isolation, Mailpit, Supabase). Use when writing, debugging, or fixing E2E tests to ensure worker isolation and stability.
PinPoint E2E Testing Skill
This skill guides you through the E2E testing infrastructure of PinPoint.
Before Writing an E2E Test (30-Second Pre-Flight)
Run this checklist BEFORE typing test("...", async ({ page }). The 2026-05 audit found that ~3/4 of new E2E specs that get filed are misallocated and could land at a cheaper, faster, more reliable layer.
- What bug class does this catch? See
pinpoint-testingskill § "Bug Classes & Cheapest Catching Layer". Class A/B/C/E/G/H/I should not be E2E — integration or RTL unit is the right home. - Does an integration or unit test already cover this feature? Run
rg -l "your-keyword" src/test/ src/first (substitute the actual feature keyword). The audit found that agents create new specs because they can't see the existing tests — most of the time you should be extending an existing file, not creating a new one. Seepinpoint-testingskill § "Where Existing Coverage Lives". - Class-J self-check (AGENTS.md §2.1 "Test What We Own"): Does this spec hit any URL outside
localhost,127.0.0.1, or our owned local stack (Mailpit, PGlite, local Supabase)? If yes → STOP. Mock the SDK at the boundary insrc/lib/<service>/client.test.ts. Live Discord, real OAuth provider redirects, vendor email-template parsing are violations. - Would the assertion be the same if I called the Server Action directly with PGlite seeded data? If yes → integration test, not E2E. The browser overhead buys nothing.
- Is this a genuine multi-step user journey that spans two or more page renders (e.g. login → mutate → verify across pages)? If no → almost certainly not E2E.
If all five say "E2E is the right layer", write it. Otherwise, the cheapest layer that catches the bug class wins.
Quick Start
- Run Smoke Tests:
pnpm run smoke(Fast, critical paths) - Run Full Suite:
pnpm run e2e:full(Comprehensive — CI only, don't run locally unless asked) - Debug Mode:
pnpm exec playwright test e2e/path/to/test.spec.ts --debug
Which Tests to Run (Decision Tree)
- Changed pure logic/utils? →
pnpm run check(unit tests, ~12s) - Changed a single E2E-relevant file? →
pnpm exec playwright test e2e/path/to/file.spec.ts --project=chromium(~15-30s) - Changed UI components/forms? →
pnpm run smoke(~60s) - Changed auth/permissions/middleware? →
pnpm run smoke+ targeted full specs - Changed DB schema/migrations? →
pnpm run preflight(full suite) - NEVER run
e2e:fulllocally unless explicitly asked — that's what CI is for
Key rules for agents:
- Always use
--project=chromiumfor targeted runs (skip Mobile Chrome unless testing responsive) - Use
--headedfor debugging visual issues pnpm run checkcatches 90% of issues — E2E is for integration verification, not iteration- If a test is flaky locally, report it — don't retry in a loop
The Golden Rule: Worker Isolation
PinPoint E2E tests run in parallel against a shared database.
YOU MUST PREVENT CROSSTALK.
- Unique Data: Never assume the DB is empty. Always create your own unique data.
- Unique Users: Do not share
admin@test.comacross parallel tests if those tests modify global state (e.g., settings, notifications). - Unique Machines: Create a fresh machine for your test.
- Unique Titles: Use
getTestIssueTitle("My Title")to prefix issues with[w0_xyz].
Common Helpers
- Select Reset Assertions: Use
assertSelectAtPlaceholder(trigger, placeholderText)for placeholder state, orassertSelectValue(trigger, expectedLabel)for default value state (e.g.await assertSelectValue(page.getByTestId("select-id"), "Minor")).
References
- Best Practices: See references/e2e-best-practices.md for structure and anti-patterns.
- Isolation Patterns: See references/isolation-patterns.md for how to use
test-isolation.tsandsupabase-admin.ts. - Helpers: See references/common-helpers.md for
actions.ts,page-helpers.ts, andmailpit.ts.
Debugging Checklist
If a test fails in CI or parallel mode:
- Crosstalk?: Is it seeing data from another worker? (Check screenshots for other prefixes).
- Fix: Use
getTestPrefix()filtering and unique resources.
- Fix: Use
- Session Lost?: Redirecting to
/report/successor/loginunexpectedly?- Fix: Ensure
x-skip-autologinis NOT interfering. Addtest.use({ storageState: STORAGE_STATE.<role> })to the describe block, or useloginAsfor mid-test role switches. Checktest.describe.serialif tests share a user.
- Fix: Ensure
- Timeout?: Waiting for a toast or email?
- Fix: Use
waitForLoadState("networkidle")before assertions. Increase timeouts for emails.
- Fix: Use
- Mobile layout different?: Nav links not visible on mobile?
- Fix: AppHeader is unified — same
data-testid="app-header"on all viewports. Nav links hide belowmd:, BottomTabBar handles mobile navigation. UsetestInfo.project.name.includes("Mobile")only when testing layout-specific behavior (e.g., checking BottomTabBar visibility).
- Fix: AppHeader is unified — same
Authentication Strategy
Decision tree for new tests:
| Test type | Auth approach |
|---|---|
| Tests one role throughout | test.use({ storageState: STORAGE_STATE.<role> }) |
| Switches roles mid-test | loginAs(page, testInfo, { email, password }) |
| Tests login/signup/password reset | No auth — start unauthenticated |
| Tests public routes | No auth — omit test.use() |
Dynamic user (created via createTestUser) |
loginAs after creating the user |
Available roles:
import { STORAGE_STATE } from "../support/auth-state"; // adjust path to e2e root
// STORAGE_STATE.admin → admin@test.com
// STORAGE_STATE.member → member@test.com
// STORAGE_STATE.technician → technician@test.com
No auth needed for unauthenticated tests — simply omit test.use().
Creating a New Test
Scaffold (single-role — preferred):
import { test, expect } from "@playwright/test"; import { STORAGE_STATE } from "../support/auth-state"; import { getTestIssueTitle } from "../support/test-isolation"; test.describe("My Feature", () => { test.use({ storageState: STORAGE_STATE.member }); test("my feature works", async ({ page }) => { const title = getTestIssueTitle("Feature Test"); await page.goto("/dashboard"); // ... }); });Scaffold (multi-role or auth flow — use loginAs):
import { test, expect } from "@playwright/test"; import { loginAs } from "../support/actions"; test("role-switch works", async ({ page }, testInfo) => { await loginAs(page, testInfo); // logs in as member // ... do member actions });Isolate: If modifying global state, create a temp user/machine in
beforeAll.Cleanup: Delete created resources in
afterAll.