---
name: creating-journey-e2e-tests
description: Derive an outcome-asserting E2E test from a user-journey contract (UJ-NNN doc or flow spec) into a Playwright-style spec covering happy path + failure/permission branches. Follows create-journeys. Triggers on "write the E2E for this journey", "derive a test from UJ-0NN", "make a real outcome test for the purchase flow".
---

# Deriving Journey E2E Tests

audience: AI coding agents first.

MUST follow `/home/user/Projects/0 DOCS/GIT_FATIGUE.md` §12. Valid current-tree receipt/log = proof; NEVER rerun broad typecheck/build/test. Minimal failing regression test during TDD is allowed.

Turn ONE journey contract into ONE outcome-asserting E2E spec. A journey doc catches zero bugs until derived into a test that drives the real UI and asserts durable outcomes. This is the pass AFTER `create-journeys` (which writes the contract). Input: a journey contract (`success_state`, happy path, alternate/failure paths, permissions). Output: a spec whose every assertion fails on a real regression.

**If the contract is weak, fix the DESIGN first — do not derive a smoke test from a smoke journey.** The design methodology (which journeys earn a test, and per-bug-class what the flow must exercise to catch flow bugs vs UI bugs) lives in `create-journeys` → "Design the journey to find bugs". A contract that names no durable truth or no real trigger is a design defect; send it back, do not paper over it with clever assertions.

## The bar: every assertion fails on a real regression

Reject any check that passes for a broken page. This is the whole point.

```ts
// DO NOT — tautologies and fail-open patterns that pass while the flow is broken:
await expect(page).toHaveURL(/.*​/);                    // reject: matches anything
await expect(page.locator('body')).toBeVisible();       // reject: proves nothing
expect(status).toBeLessThan(500);                       // reject: 401/403/302 pass
if (await el.isVisible()) { await el.click(); }          // reject: silently skips
expect(s.payment_status).toMatch(/paid|complete|succeed/); // reject: OR-regex hides wrong status

// DO — one expected state, one exact assertion, visible + durable:
await expect(page.getByTestId('purchase-moment-summary')).toBeVisible();
await expect(page.getByTestId('purchase-moment-summary')).toContainText(deal.title);
expect(snapshot.payment_status).toBe('paid');           // exact durable truth
expect(snapshot.voucher_status).toBe('UNREDEEMED');
```

## Derivation ladder

For the ONE journey, produce these tests in order. Stop only when the contract has no more branches.

1. **Happy path → the full `success_state`.** Drive the journey's real trigger, reach the terminal state, assert BOTH halves of `success_state`:
   - **Visible confirmation** — the elements the user sees, by stable `data-testid`/accessible-name (never DOM ordinal). Assert the specific values the contract names (the deal title, the *charged amount* — not just "an amount").
   - **Durable truth** — re-read the persisted state (DB snapshot / authoritative API) and assert the exact status the contract names (`paid`, `UNREDEEMED`), not an OR-regex.
   - **Persistence** — reload the terminal page; assert the confirmation re-renders from persisted state (catches optimistic-UI that lies).
2. **Each alternate/failure branch → its own assertable end.** One test per branch (decline, expired, sold-out, request failure, still-processing). Assert the visible error AND that NO durable side-effect occurred (no paid row, no voucher). A failure test that only checks the error message misses half the bug surface.
3. **Permissions/boundaries → deny at every layer the contract names.** Non-owner and anonymous access: assert the UI denial (redirect to login / access-denied screen, no leaked data) AND the API-level authz the contract states (e.g. `GET /api/.../[id]` → `404`/`403` for a non-owner). Page-only checks miss IDOR at the API.

## Drive the real trigger

Start at the journey's documented entry (the deal page + Buy Now button), not a shortcut route that skips the UI under test. Jumping straight to `/buy/<id>` or the terminal route bypasses the very control (button/modal) that most often breaks. Shortcut only a *precondition* that is covered by its own test, and say so in a comment.

## Skips: env-precondition only, NEVER failure-hiding

`test.skip` is legitimate ONLY when a genuine environment precondition is absent (no seeded fixture exists in the target DB) — a data gap, not a product defect. NEVER skip on a signal that would also fire when the feature is broken:

```ts
// OK — precondition genuinely absent (env data gap):
const deal = await firstBuyableDeal('COUPON');
if (!deal) test.skip(true, 'No ACTIVE Stripe-enabled COUPON fixture in target DB.');

// DO NOT — this also fires when checkout is BROKEN, so skipping hides the bug:
const reached = await stripeFrame.waitFor(...).then(() => true, () => false);
if (!reached) test.skip(true, 'checkout did not reach PaymentElement'); // reject: assert instead
await expect(stripeFrame).toBeAttached({ timeout: 45_000 });            // DO: fail on absence
```

Prefer seeding a deterministic fixture over discovering one, so the happy path can't silently skip.

## Reuse the project's journey harness — never re-mint

Use the repo's existing E2E helpers; do not hand-roll auth or DB access. In this repo: `apps/web/tests/e2e/journeys/_journey-helper.ts` — `injectSession(context, userId)` (signed-JWT cookie), `firstBuyableDeal(type)`, `createFreshPurchase(kind, …)`, `purchaseSnapshot(purchaseId)`, `sql()`. Stripe card entry: `apps/web/tests/e2e/purchase-flows/helpers/stripe-elements.ts` `fillStripeCard(page)`. Live runs are gated (`ALLOW_LIVE_E2E=1`, `--config=playwright.live.config.ts`) — see the `md-verify` skill for auth-claim, timeout, and networkidle rules. New spec goes in `apps/web/tests/e2e/journeys/` (git-tracked; the `flowmap/` tree is gitignored capture-only).

## Traceability

Header comment names the source journey doc + maps each section (happy / alt-N / permissions) to the test that covers it, and points to sibling specs for branches already covered elsewhere (avoid duplicate coverage). Worked reference in this repo: `apps/web/tests/e2e/journeys/UJ-012-single-deal-purchase.spec.ts` (contract: `docs/user_journeys/customer-portal/UJ-012-…md`).

## Before declaring the test done

- [ ] No tautology / range / OR-regex / fail-open-`if` assertion remains.
- [ ] Happy path asserts visible confirmation + exact durable status + reload persistence.
- [ ] Each failure branch asserts visible error AND absence of durable side-effect.
- [ ] Permission branch asserts UI denial AND API-level authz status.
- [ ] Real trigger driven (not a route shortcut past the UI under test).
- [ ] Skips guard only absent env preconditions, never broken-feature signals.
- [ ] Valid current-tree receipt proves typecheck + discovery; absent receipt → designated executor runs the affected lane once.
