Files
collective-lists/plan/fase-7-collective-flow-tests.md
Oier Bravo Urtasun 17bc344986 docs(fase-7): plan — collective-flow E2E coverage + ambrosio deploy notes
- plan/fase-7-collective-flow-tests.md: 14 new Playwright tests (O-01..O-04
  onboarding, I-01..I-05 invitation acceptance, MC-01..MC-05 admin manage)
  plus shared fixtures/db.ts helpers (resetEva, seedExpiredInvitation,
  seedUsedInvitation, restoreSeedMembership). Risks + scope-out documented.
- CLAUDE.md: add "Fase 6+ production deploy to ambrosio" section with the
  files / config / discovered fixes for the prod stack; link to the new plan
  from the Project Status line; add gotchas #16 (injectManifest filename
  hardcoded in @vite-pwa/sveltekit 0.6.8) and #17 (supabase/postgres doesn't
  bootstrap its internal roles on a fresh volume).
2026-04-14 21:39:26 +02:00

8.5 KiB

Fase 7 — Collective creation flow: fill the E2E gap

Estado: 📝 Plan (2026-04-14). Objetivo: cerrar la laguna de cobertura UI en el ciclo completo de un colectivo — onboarding, invitación y gestión — que hoy sólo vive en RLS / pgTAP.

Contexto: el audit de 2026-04-14 (ver mensaje de planificación en el chat) muestra que el backend del dominio "colectivo" está bien testado (13 casos RLS + 7 pgTAP), pero la UI sólo tiene 2 asserts (sidebar tras login en auth.test.ts A-04/A-06). Ninguna prueba recorre /onboarding, /invitation/[token] ni /collective/manage.

Esta fase no introduce features — sólo tests. Si alguno falla en el primer run es porque hay un bug real a corregir (los flujos están en producción desde Fase 1).


7.0 Helpers compartidos

apps/web/tests/fixtures/db.ts (nuevo) — usa el admin service-role client de @colectivo/test-utils para manipulación directa (bypass RLS):

  • resetEva() — borra todos los collective_members de Eva y los collectives que Eva haya creado. Se invoca en beforeEach de onboarding.test.ts y invitation.test.ts (ambos asumen Eva = cero colectivos al empezar).
  • seedExpiredInvitation(collectiveId: string, adminId: string): Promise<string> — inserta una fila en collective_invitations con expires_at = now() - 1 hour; devuelve el token. Usado por I-04.
  • seedUsedInvitation(collectiveId: string, adminId: string, acceptorId: string): Promise<string> — inserta con used_at = now() y used_by = acceptorId. Usado por I-05.
  • restoreSeedMembership() — restituye los roles base de la semilla (Borja = member, Carmen = member, David = guest). afterAll de manage-collective.test.ts, imprescindible para que los tests posteriores (lists / items / tasks / notes) vean el estado sembrado esperado.

Patrón de importación para evitar que helpers se filtren al bundle del cliente: el helper vive bajo tests/fixtures/, no src/, y sólo se importa desde ficheros *.test.ts.


7.1 /onboarding — O-series (4 tests, fichero nuevo)

Fichero: apps/web/tests/e2e/onboarding.test.ts Fixture: Eva (USERS.eva), sin colectivo al iniciar (garantizado por resetEva() en beforeEach).

  • O-01 — Eva se loguea, no tiene colectivos → el layout la redirige automáticamente a /onboarding. Asserta page.url() contiene /onboarding y la pantalla muestra el formulario de creación.
  • O-02 — Camino feliz: escribe un nombre ("Familia Eva"), selecciona un emoji (🏠), envía → redirección a /lists + sidebar muestra el nuevo colectivo con nombre y emoji correctos + localStorage.getItem('activeCollectiveId') es el id devuelto por el INSERT.
  • O-03 — Input de nombre vacío → botón de submit deshabilitado (o error inline). Ninguna petición POST sale (network stub o count collective_members antes/después sin cambios).
  • O-04 — Tras O-02, Eva crea un segundo colectivo desde la UI (botón "+" en el switcher o entrada del sidebar). El switcher muestra ambos; seleccionar el segundo persiste activeCollectiveId tras recargar.

Criterio de aceptación: los 4 tests pasan; Eva queda sin colectivos al final (via afterAll → resetEva()).


7.2 /invitation/[token] — I-series (5 tests, fichero nuevo)

Fichero: apps/web/tests/e2e/invitation.test.ts Fixtures: Ana (admin del colectivo semilla) + Eva (no-miembro). resetEva() en beforeEach.

  • I-01 — Ana abre /collective/manage, pulsa "Generar invitación" → un URL con token aparece y es copiable; la fila correspondiente en collective_invitations tiene expires_at > ahora y used_at IS NULL.
  • I-02 — Eva (deslogueada) abre el URL de invitación activo → el guard redirige a login; tras autenticarse vuelve a la página de aceptación; pulsa "Aceptar" → queda registrada como member en collective_members + redirigida al colectivo (/lists del nuevo collective_id).
  • I-03 — Eva (ya logueada, aún no-miembro) abre el URL → un solo click de "Aceptar" la añade como member, sin pasar por login.
  • I-04 — Ana genera invitación, Eva abre con token expirado (vía seedExpiredInvitation) → la UI muestra estado "expirada" sin botón de aceptar; no se inserta fila en collective_members.
  • I-05 — Invitación ya usada (seedUsedInvitation) → la UI muestra estado "ya aceptada" / "token inválido"; igual, no hay INSERT.

Captura del token en I-02/I-03: Ana abre /collective/manage en la misma browser context, pulsa "Generar", el token se lee del href del link copiable (selector data-testid="invite-link"). La parte de Eva se hace en un segundo browser context (browser.newContext()), aislando cookies/localStorage.

Criterio de aceptación: los 5 tests pasan; Eva y sus membresías quedan reseteadas al terminar.


7.3 /collective/manage — MC-series (5 tests, fichero nuevo)

Fichero: apps/web/tests/e2e/manage-collective.test.ts Fixtures: Ana (admin), Borja (member), Carmen (member) sobre el colectivo semilla. restoreSeedMembership() en afterAll.

  • MC-01 — Ana renombra el colectivo a "Familia Bravo-Urtasun" → el sidebar actualiza optimistamente + la fila en collectives.name coincide tras reload.
  • MC-02 — Ana promueve a Borja de member a admin → tras reload, Borja ve el botón "Generar invitación" y el input editable de nombre (afordances reservadas a admin).
  • MC-03 — Ana degrada a Borja de vuelta a member. Precondición: Ana sigue siendo admin (no dispara el trigger de auto-promoción). Asserta que el cambio persiste.
  • MC-04 — Ana elimina a Carmen → la fila desaparece de la lista inmediatamente. En un segundo context, Carmen (logueada antes de la eliminación) intenta abrir /collective/manage → debe recibir redirección / 403 / estado vacío (el RLS ya bloquea; hay que comprobar que la UI no crashea).
  • MC-05 — Ana genera una invitación, luego pulsa "Revocar" en esa fila → la fila se marca como revocada (o desaparece); abrir el URL revocado muestra el mismo estado de "inválida" que I-05.

Criterio de aceptación: los 5 tests pasan; restoreSeedMembership() deja Borja=member, Carmen=member, David=guest antes de que se ejecute cualquier otro test.


7.Z Verificación

  • just test-e2e → 14 tests nuevos verdes, 0 regresiones en los 46 existentes (total: 60 Playwright).
  • El fichero restoreSeedMembership() no deja cambios residuales — verificación manual: docker exec colectivo-dev-db-1 psql -U postgres -c "SELECT user_id, role FROM collective_members ORDER BY user_id" debe devolver los 4 miembros de la semilla con sus roles originales.
  • Actualizar CLAUDE.md con el nuevo total de tests + una línea describiendo la cobertura añadida.

Criterio de aceptación: 14 tests verdes, seed restaurada, doc actualizada.


Riesgos / notas

  1. Eva como fixture compartido entre onboarding.test.ts e invitation.test.ts. Si Playwright paraleliza ambos ficheros, el resetEva() del segundo puede borrar los datos mientras el primero está a medias. Mitigación: los 3 ficheros nuevos deben ejecutarse secuencialmente. playwright.config.ts ya usa workers: 1 por defecto en CI; confirmar que sigue así al añadir estos tests.

  2. restoreSeedMembership() es un punto único de fallo. Si se olvida el afterAll o falla, toda la suite aguas abajo falla con "member not found". Mitigación: envolver los tests MC dentro de un test.describe.serial(...) y registrar el afterAll como hook inline, no importado.

  3. I-04 e I-05 manipulan tablas directamente vía admin client. Esto bypassa el trigger de auditoría si se añade en el futuro. Mitigación: documentar en el helper que es la única vía aceptable; cualquier nuevo trigger auditor debe tolerarlo (o generar eventos equivalentes manualmente).

  4. Captura del token de invitación desde la UI (I-02/I-03). Depende de que el input del link tenga un data-testid estable. Si hoy no existe, hay que añadirlo a apps/web/src/routes/(app)/collective/manage/+page.svelte como parte del trabajo (cambio mínimo, no es scope creep).


Scope explícito fuera

  • No se tocan tests de shopping / tasks / notes.
  • No se cubre la flow de "rechazar invitación" (no existe en la UI).
  • No se cubre el trigger auto-promote-oldest-admin desde E2E — pgTAP ya lo testa (003_promote_on_admin_leave.sql), duplicarlo en Playwright no aporta.
  • No se cubren notificaciones push al aceptar invitación — fuera de MVP.