E2E Signup Testing
Purpose
The signup E2E suite validates the end-to-end user journey from landing on the sign-up page through completing onboarding and reaching the dashboard. It runs against a real Vercel preview deployment using real Clerk test-mode auth and a real database, so it exercises the full stack — Next.js routing, Clerk auth, Brandfetch enrichment, and Stripe Connect onboarding — exactly as a new nonprofit admin would experience it.
The suite exists because the signup flow involves complex multi-step state (Clerk session, DB onboarding flags, Stripe account readiness) that is difficult to cover with unit tests alone. A green run here means a real user can sign up without hitting a dead end.
CI impact: These tests run on every Vercel preview deployment and block merges if they fail (see Running in CI).
What's covered
The suite contains 6 scenarios:
| File | Scenario | What it validates |
|---|---|---|
happy-path.spec.ts | Happy path — nonprofit domain | Full signup with a nonprofit email domain; Brandfetch enrichment runs; org is created; user reaches /home. |
generic-email.spec.ts | Generic email (manual entry path) | Signup with a public-email domain (gmail.com); Brandfetch is skipped; org name must be entered manually. |
already-signed-in.spec.ts | Already-signed-in guard | After completing signup, navigating to /sign-up must redirect to /home. |
resume.spec.ts | Mid-flow resume | Sign up, complete onboarding step 1, close context. Re-sign-in, assert we land at onboarding step 2 (not step 1 or /home). |
stripe-connect-resume.spec.ts | Stripe Connect resume | Sign up, reach the Stripe Connect step, close context. Re-sign-in, click "Resume Stripe setup", assert the redirect is a Stripe account link URL. |
demo-redirect.spec.ts | Demo hostname redirect | A request to demo.givelink.ai/sign-up must redirect to app.givelink.ai/sign-up. Tests proxy.ts DEMO classification via Host-header override. |
impersonation-roundtrip.spec.ts | Impersonation round-trip | Full Clerk actor-token impersonation cycle: seed fixture, impersonate, return to admin. Validates admin_impersonations DB row and admin_audit_log entries. (Spec D §12.1) |
Note:
impersonation-roundtrip.spec.tsis included in the signup suite because it requires the same real-Clerk, real-DB environment as the signup scenarios. Other impersonation invariants stay in Vitest (Spec §12.6).
Running locally
The suite requires a running dev server (or a preview URL). The dev server
auto-picks a port in the 3006–3199 range based on the worktree; check the
terminal output for the assigned port.
# From the repo root — start the dev server first
pnpm dev
# In a second terminal — run the full signup suite
E2E_BASE_URL=http://localhost:<PORT> TEST_TEARDOWN_SECRET=<secret> pnpm test:e2e:signupTEST_TEARDOWN_SECRET must match the value in your .env.local. It is
required — the suite calls teardown endpoints after each test and will throw
if the secret is missing.
To run a single scenario during debugging:
E2E_BASE_URL=http://localhost:<PORT> TEST_TEARDOWN_SECRET=<secret> \
pnpm exec playwright test tests/e2e/signup/happy-path.spec.tsTo open the Playwright trace viewer after a local failure:
pnpm exec playwright show-trace test-results/<run-id>/trace.zipRunning in CI
The workflow file is .github/workflows/e2e-signup.yml. It triggers on every
successful Vercel Preview deployment:
on:
deployment_status:
jobs:
e2e:
if: |
github.event.deployment_status.state == 'success' &&
github.event.deployment_status.environment == 'Preview'The job sets E2E_BASE_URL to the Vercel preview URL from the deployment
event, installs Chromium, and runs pnpm test:e2e:signup. No local build
or web server is needed — the suite hits the live preview deploy.
On failure, the job uploads the Playwright report as a GitHub Actions artifact
named playwright-report with a 7-day retention. Download it and open
index.html to see the HTML report, or use the trace files with
playwright show-trace.
Secrets required in the repo:
TEST_TEARDOWN_SECRET— authorizes the teardown endpoints. Set in GitHub repo → Settings → Secrets → Actions.ALLOW_TEST_TEARDOWN=true— set as an Actions environment variable (not a secret) for the preview environment. The teardown endpoints refuse all requests when this is unset.
Test accounts
Clerk test-mode automatically verifies email addresses whose local-part
ends with +clerk_test (literal). GiveLink's uniqueClerkTestEmail() helper
in tests/e2e/signup/helpers.ts generates unique addresses for each run:
// Pattern: givelink_<timestamp>-<nonce>+clerk_test@<domain>
// Example: givelink_1713499200000-abc12345+clerk_test@hopefoundation-test.org
export function uniqueClerkTestEmail(domain = "hopefoundation-test.org"): string {
const nonce = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
return `givelink_${nonce}+clerk_test@${domain}`;
}The timestamp + random nonce ensures no two parallel test runs collide. Always use this helper — don't hardcode test emails, as Clerk enforces uniqueness and reusing an email across runs causes sign-up failures.
The impersonation-roundtrip.spec.ts scenario uses the
seedImpersonationFixture API (see Teardown) to create both an
admin user and a target user. These are also +clerk_test accounts.
Teardown
Every test that creates a Clerk user or org is responsible for cleaning up
after itself — whether the test passes or fails. The suite calls three
/api/test-admin/admin/ endpoints, all triple-gated:
- Triple gate on every endpoint:
ALLOW_TEST_TEARDOWNenv var must be"true"(absent in production).- The request must carry the
x-test-secretheader matchingTEST_TEARDOWN_SECRET. - The app must not be running with
VERCEL_ENV=production.
The four test endpoints are:
| Endpoint | Method | Purpose |
|---|---|---|
/api/test-admin/admin/teardown | POST | Delete a user by email — Clerk + DB cascade. Used by teardownUser() in helpers.ts. |
/api/test-admin/admin/seed-impersonation-fixture | POST | Create admin + target Clerk users, seed DB rows. Returns ImpersonationFixture. |
/api/test-admin/admin/impersonation-state | GET | Return admin_impersonations and admin_audit_log rows for a target user ID. Used to assert state after impersonation. |
/api/test-admin/admin/teardown-fixture | POST | Delete the admin user, target user, and org created by seed-impersonation-fixture. |
The helpers.ts functions for each:
teardownUser(request, email) // → POST /teardown
seedImpersonationFixture(request) // → POST /seed-impersonation-fixture
getImpersonationState(request, userId) // → GET /impersonation-state?targetUserId=...
teardownImpersonationFixture(request, fx) // → POST /teardown-fixturecallTestApi (internal) handles 429 (Clerk rate-limit) with a 2 s backoff and
409 (seed collision, near-impossible with nonces) with a 50 ms retry. All other
errors fail immediately.
Debugging failures
Download the Playwright report from GitHub Actions
- Go to the failed workflow run in GitHub → Actions.
- Scroll to Artifacts at the bottom → download
playwright-report.zip. - Unzip and open
index.htmlin a browser. - Click a failing test → Traces tab → open the trace viewer.
Common failure classes
| Symptom | Likely cause | Fix |
|---|---|---|
[timeout] waiting for URL to match /home/ | Brandfetch is down or rate-limiting. The enrichment step spins indefinitely when Brandfetch returns nothing. | Retry once. If persistent, check Brandfetch status page. |
Teardown failed: 401 | TEST_TEARDOWN_SECRET mismatch or ALLOW_TEST_TEARDOWN not set on the preview environment. | Verify the Vercel preview env vars and GitHub secret match. |
Teardown failed: 429 after fixture seed | Clerk rate-limiting test-mode account creation. The 2 s backoff in callTestApi handles one retry. Persistent 429 means too many parallel runs. | Reduce parallelism or wait 60 s. |
expect(url).toMatch(/stripe.com/) fails | Stripe returned an error creating the account link. Usually a test-mode credential issue. | Check STRIPE_SECRET_KEY on the preview environment — must be a test-mode key. |
Already signed in test flaky | Clerk session cookie persisted across tests. The scenario isolates context per test — confirm storageState is not leaking. | Check playwright.config.ts for shared auth state. |
demo-redirect.spec.ts fails in prod-like env | proxy.ts DEMO classification depends on the Host header override. Some preview configurations strip custom headers. | Confirm the preview URL is not mapped to demo.givelink.ai. |
Trace viewer key actions
- Network tab — find the failing API call (Brandfetch, Clerk, Stripe) and its response.
- Console tab — Next.js server errors surface here.
- Actions tab — replay the test step-by-step to see where it diverged.
Adding a new scenario
- Create a new
.spec.tsfile intests/e2e/signup/. - Import
uniqueClerkTestEmail,teardownUser, andwaitForUrlMatchingfrom./helpers. - Use
test.afterEachto callteardownUser(request, email)— even on failure (Playwright always runsafterEach). - Add a comment at the top of the file citing which part of
docs/signup-flow.mdit exercises (see existing specs for the format). - Run locally with
pnpm test:e2e:signupto confirm it passes before pushing. - The CI workflow picks up new files automatically — no workflow config changes needed.
If the new scenario requires an admin user or impersonation fixtures, use
seedImpersonationFixture and teardownImpersonationFixture rather than
creating accounts manually — they handle Clerk rate-limit retries and ensure
clean teardown.
Troubleshooting
TEST_TEARDOWN_SECRET is required for E2E teardown
The TEST_TEARDOWN_SECRET env var is not set. Add it to your .env.local
(for local runs) or to the GitHub repo secrets (for CI). The value must match
TEARDOWN_SECRET in the app's Vercel env.
pnpm test:e2e:signup exits immediately with no tests run
The Playwright config (playwright.config.ts) requires E2E_BASE_URL to be
set when not in local-server mode. Set the variable to your dev server URL.
Teardown failed: 403 — endpoint reachable but rejected
ALLOW_TEST_TEARDOWN is not set to "true" on the target environment. For
preview environments, add this as an environment variable in Vercel project
settings (not a secret). For local runs, add it to .env.local.
CI run never triggers
The workflow fires on deployment_status events from Vercel. If no run
appears after a preview deploy, check:
- The Vercel GitHub integration is connected (repo settings → Integrations).
- The deployment status event is firing — check the GitHub repo → Actions → search for "E2E Signup Tests" in the workflow list.
- The deployment's environment is exactly
"Preview"(case-sensitive).
Teardown leaves orphaned Clerk users after a crash
If the dev server crashed mid-test, teardown may not have run. Find the
orphaned accounts in the Clerk dashboard (filter by email pattern
givelink_*+clerk_test@) and delete them manually. In CI, orphaned accounts
from a failed run clean up automatically on the next run because each email
is unique — they don't interfere with future tests.
References
- Test files:
tests/e2e/signup/ - Helpers:
tests/e2e/signup/helpers.ts - CI workflow:
.github/workflows/e2e-signup.yml - Playwright config:
playwright.config.ts - Teardown endpoints:
src/app/api/test-admin/admin/ - Spec:
docs/superpowers/specs/2026-04-18-spec-d-e2e-and-hard-deletes.md§12