# Fase 7 — collective-flow E2E coverage (2026-04-14)

12 new Playwright tests + 1 DB fixtures helper + 1 migration. Writing the tests uncovered three product-code bugs which are fixed as part of this phase.

## Tests (all green, included in `just test-e2e`)

- `apps/web/tests/e2e/onboarding.test.ts` — O-01..O-03: Eva auto-redirect to `/onboarding`, happy-path create, empty-name submit-disabled.
- `apps/web/tests/e2e/invitation.test.ts` — I-01..I-05: admin generates token, logged-out-then-accept, logged-in one-click accept, expired + already-used error states.
- `apps/web/tests/e2e/manage-collective.test.ts` — MC-02..MC-05: promote member → admin, demote back, remove member, generate pending invitation.
- `apps/web/tests/fixtures/db.ts` — `resetEva()` / `seedExpiredInvitation()` / `seedUsedInvitation()` / `restoreSeedMembership()` / `countMembership()`. All via raw SQL (bypasses RLS) so they don't depend on `SUPABASE_SERVICE_ROLE_KEY` being in the Playwright process env.

## Scope dropped from the plan

- **O-04** (multi-collective via a "+ new collective" button in the sidebar): the affordance does not exist in the UI; adding it would be feature work, out of a test-only phase.
- **MC-01** (rename collective): `/collective/manage` has no rename input; same reasoning.

Both are flagged in `plan/fase-7-collective-flow-tests.md` as future UI follow-ups.

## Product-code fixes discovered while writing these tests (all committed with this phase)

1. **`supabase/migrations/012_create_collective_rpc.sql` — atomic `create_collective(name, emoji)` RPC.** Pre-existing onboarding flow did two separate REST calls (`INSERT collectives` chained with `.select().single()`, then `INSERT collective_members`). Both failed: the RETURNING on the first triggered the SELECT policy (`is_member(id)`) which the creator can't pass yet, and the second was gated by `is_admin(collective_id)` against a collective with zero admins. The RPC is `SECURITY DEFINER` and does both inserts atomically. `onboarding/+page.svelte` now calls `supabase.rpc('create_collective', {...})`.

2. **`routes/+layout.svelte` — resume pending invitation after login.** `invitation/[token]/+page.svelte` stashes the token in `sessionStorage.pendingInvitationToken` before triggering OAuth, but nothing read it back. Post-login redirect logic now checks sessionStorage and routes the user back to `/invitation/<token>` instead of defaulting to `/onboarding` or `/lists`.

3. **`routes/(app)/collective/manage/+page.svelte` — reactively reload members.** `onMount` called `loadMembers()` once; if `$currentCollective` was still null at that point (race with the auth listener on a cold navigation), the query short-circuited and the list stayed empty. Now subscribed to `currentCollective` so `loadMembers` re-runs whenever the store resolves.

4. **`routes/invitation/[token]/+page.svelte` — wait for authLoading before deciding.** Racing with the auth listener's first emit caused the page to see `!$isAuthenticated` for a freshly-authenticated user and trigger a redundant Keycloak round-trip. Now awaits `authLoading → false` before branching.

## Test IDs added to UI (stable selectors for Playwright)

- Onboarding: `onboarding-create-tab`, `onboarding-join-tab`, `collective-name-input`, `collective-submit`.
- Manage: `member-row-<uid>`, `role-select-<uid>`, `remove-member-<uid>`, `generate-invite`, `invite-link`.
- Invitation: `invitation-accept`, `invitation-error` (with `data-error-key` attribute carrying the error key for assertion).
