Files
collective-lists/plan/fase-2b-realtime-modo-compra.md
Oier Bravo Urtasun f396897cb5 test: full test suite (Vitest + pgTAP + Playwright) + TDD plan restructure
Add a 4-layer test stack covering RLS, triggers, and UI flows for Fases 0–2a,
then restructure every plan file so future fases start with tests and end with
a verification gate.

Test suite
- packages/test-utils: Vitest integration tests signing HS256 JWTs via jose so
  each test acts as a specific seed user (createClientAs + createAdminClient)
- supabase/tests: pgTAP for accept_invitation(), item_frequency trigger, and
  promote-on-admin-leave; each file self-installs pgtap extension
- apps/web/tests: Playwright E2E with live Keycloak login per test (storageState
  caching doesn't rehydrate Supabase's session state reliably)
- just test-all chains the three suites; test-db forwards POSTGRES_PASSWORD
  as PGPASSWORD with ON_ERROR_STOP=1 so failures abort the chain

Supabase auth gotcha
- PostgREST queries inside onAuthStateChange deadlock on GoTrue's navigator.locks
  auth lock (getAccessToken → getSession → initializePromise waits on the same
  lock that's held during event dispatch). Fix is two defenses: a pass-through
  lock in $lib/supabase, and a setTimeout(0) defer in root +layout.svelte to
  push loadUserCollectives out of the callback's microtask chain. Either alone
  is insufficient; both together unblock the Playwright suite.

Env key rotation
- apps/web/.env.development had a stale demo anon key signed with a different
  secret than root .env; Vite inlined that into the browser bundle so Kong (which
  uses the root .env value) rejected every request with 401. Aligned the two
  files and added a memory entry to flag this for the next rotation.

Plan restructure (TDD)
- Every fase now opens with X.0 Tests primero and closes with X.Z Verificación
  final. Completed fases (0, 1, 2a) show the pattern retroactively with the
  tests that currently cover them; pending fases (2b, 3, 4) list the tests to
  write before implementation.

Docs
- CLAUDE.md status line reports 54 + 12 + 15 = 81 green tests, adds gotchas
  #11 (auth-lock deadlock) and #12 (no storageState caching)
- README.md adds a TDD methodology section and the test-all command
- .gitignore excludes Playwright's generated reports and auth state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:40:16 +02:00

5.5 KiB
Raw Blame History

Fase 2b — Sincronización en Tiempo Real y Modo Compra

Estado: Pendiente Duración estimada: 23 semanas
Objetivo: el diferencial del producto. Es la fase más compleja técnicamente.

Esta fase sigue TDD: primero los tests, al final la verificación. Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (just test-all).

2b.0 Tests primero — escribir antes de implementar

Vitest (packages/test-utils/tests/):

  • realtime-presence.test.ts — dos clientes (Ana + Borja) entran en la misma lista; ambos aparecen en el canal Presence; salir del canal elimina al usuario
  • realtime-postgres-changes.test.ts — Ana hace INSERT/UPDATE/DELETE en shopping_items; Borja (suscrito al canal) recibe el evento en < 1s
  • realtime-isolation.test.ts — Ana y David (guest del mismo colectivo) están suscritos a la misma lista; Eva (fuera del colectivo) NO recibe nada aunque intente suscribirse (RLS en supabase_realtime.broadcast)
  • sync-queue.test.ts (unit, con mock de idb) — cada mutación escribe en pending_ops antes de llamar a Supabase; al recibir la confirmación se elimina de la cola; si falla se mantiene para reintento
  • sync-flush.test.ts (unit) — al disparar el evento online se flushea la cola respetando el orden original; colisiones resueltas con last-write-wins

Playwright (apps/web/tests/e2e/):

  • session.test.ts — S-01: abrir Modo Compra en /lists/[id]/session muestra el layout full-screen sin sidebar
  • session.test.ts — S-02: marcar un ítem lo mueve a la sección CHECKED con animación (no se duplica)
  • session.test.ts — S-03: botón "Finish shopping" → confirmación modal → lista pasa a completed, redirige a /lists
  • realtime.test.ts — R-01 (dos contextos de navegador): Ana crea ítem, Borja lo ve sin refrescar la página
  • realtime.test.ts — R-02: Ana marca ítem, avatar de Borja aparece en el indicador de Presence
  • offline.test.ts — O-01: con context.setOffline(true), añadir/marcar ítems sigue funcionando localmente y aparece el banner "sin conexión"
  • offline.test.ts — O-02: al volver online (setOffline(false)), los cambios pendientes se sincronizan y el banner desaparece

pgTAP (supabase/tests/):

  • 004_realtime_rls.sql — verificar que realtime.messages respeta las políticas RLS heredadas de shopping_items (un miembro sólo ve cambios de sus colectivos)

2b.1 Sincronización Realtime

  • Suscripción a Postgres Changes de Supabase Realtime sobre shopping_items filtrada por list_id
  • Suscripción a Presence de Supabase Realtime para indicador de miembros presentes en lista
  • Store Svelte realtimeSync:
    • Gestiona el ciclo de vida de las suscripciones (subscribe/unsubscribe al entrar/salir de la lista)
    • Aplica cambios remotos al estado local sin causar bucles
    • Detecta conflicto last-write-wins: si el updated_at remoto > local, aplica remoto y muestra aviso visual

2b.2 Offline-First

  • Instalación y configuración de idb (wrapper de IndexedDB)
  • Esquema local en IndexedDB: pending_ops, lists_cache, items_cache
  • Cola de operaciones pendientes: cada mutación se escribe en pending_ops antes de llamar a Supabase
  • Service Worker (@vite-pwa/sveltekit + Workbox):
    • NOTA TÉCNICA: SvelteKit intercepta src/service-worker.ts y bloquea imports externos (solo permite $service-worker y $env/static/public). Para usar Workbox con injectManifest hay que nombrar el fichero src/sw.ts (SvelteKit no lo reconoce como SW) y configurar strategies: 'injectManifest', filename: 'sw.ts' en vite.config.ts. Actualmente el plugin usa generateSW (SW auto-generado, precaching básico).
    • Cache-first para assets estáticos
    • Network-first con fallback a caché para rutas de la app
    • Background sync para flush de pending_ops al recuperar conexión
    • Fallback manual online event (Safari no soporta Background Sync API)
  • Fallback manual de sync: al detectar online en el evento del navegador, flush de la cola (cubre Safari que no soporta Background Sync API)
  • Indicador de estado de sincronización en la UI: synced | syncing | offline

2b.3 Modo Compra

  • Ruta /lists/[id]/session — UI dedicada al Modo Compra:
    • Ítems en formato grande (mínimo 64px de altura), optimizado para pulsar con el pulgar
    • Reordenamiento automático al marcar: pendientes arriba, marcados abajo (animación fluida con flip)
    • Indicador de presencia: avatares de miembros que tienen la lista abierta simultáneamente
    • Banner sin conexión cuando no hay red (los cambios se guardan en local)
    • Botón prominente "Finish shopping" — confirmación modal → lista pasa a completed
    • Notificación push (si el usuario ha dado permiso) a otros miembros al finalizar
  • Rendimiento: 60fps en gama media, tap < 100ms (validar en Chrome DevTools con throttling)

2b.Z Verificación final — todos los tests verdes

  • just test-all → 0 failures, incluyendo los nuevos de 2b.0
  • Prueba manual de sincronización: dos dispositivos, misma lista, cambios visibles en < 1 segundo
  • Prueba manual de offline: desconectar red, mutar, reconectar, verificar sync

Criterio de aceptación: dos personas en el mismo supermercado, con cobertura inestable, pueden marcar ítems desde sus móviles y verse los cambios mutuamente en tiempo real. Si se pierde la conexión, los cambios locales se sincronizan al recuperarla. Toda la suite pasa con just test-all.