diff --git a/.gitignore b/.gitignore index c66a29b..d8dc322 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ supabase/.temp/ # Node node_modules/ +# Playwright — reportes/snapshots, pueden contener credenciales de .env.test en el DOM +test-results/ +playwright-report/ +blob-report/ + # Next.js build output .next/ diff --git a/package-lock.json b/package-lock.json index fb39343..cb881cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@axe-core/cli": "^4.11.3", + "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4.1.8", "@types/node": "^22.0.0", "@types/react": "^19.0.0", @@ -1041,6 +1042,22 @@ "node": ">= 10" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -2135,6 +2152,20 @@ "node": ">= 6" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3002,6 +3033,38 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", diff --git a/package.json b/package.json index 5c5b6f7..c68d178 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@axe-core/cli": "^4.11.3", + "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4.1.8", "@types/node": "^22.0.0", "@types/react": "^19.0.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d280809 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@playwright/test' + +// .env.test nunca se commitea (ver .gitignore) — credenciales de un usuario +// dedicado de e2e, nunca las del director. +process.loadEnvFile('.env.test') + +export default defineConfig({ + testDir: 'tests/e2e', + use: { + baseURL: process.env.E2E_BASE_URL, + }, + // Local contra producción — no corre en CI (backlog). + reporter: 'list', +}) diff --git a/sprints/sprint-5/ETAPA_ADMIN_PROMPT.md b/sprints/sprint-5/ETAPA_ADMIN_PROMPT.md new file mode 100644 index 0000000..51887ed --- /dev/null +++ b/sprints/sprint-5/ETAPA_ADMIN_PROMPT.md @@ -0,0 +1,154 @@ +# ETAPA ADMIN — Vista de respuestas mejorada + vista detallada + +## 1. Objetivo +Mejorar `/admin/responses` para que sea una interfaz de administración real, +y crear `/admin/responses/[id]` como vista detallada de una respuesta individual. +Todo detrás del middleware de auth existente. + +## 2. Contexto +- Estado actual: `app/admin/responses/page.tsx` (Server Component, 249 líneas) + + `app/admin/responses/responses-table.tsx` (Client Component, 98 líneas). +- Auth: ya implementada — getSupabaseSession() con anon key + cookies, respeta RLS. +- Exclusión de sensibles: `is_sensitive=false` y `type != 'text_long'` en query de + questions — NO tocar, mantener exactamente igual. +- SURVEY_OPTIONS hardcodeadas con UUIDs literales — reemplazar por query dinámica. +- LIMIT 500 sin paginación real — reemplazar por paginación con page size de 50. + +## 3. DECISIONES YA TOMADAS — no consultar +- Paginación: page size 50, parámetro `?page=N` en la URL (Server Component, + recarga completa — no client-side infinite scroll). +- Filtro de survey: `` con onChange que navega a `?survey_id=`. +Opción "Todos los surveys" con value=''. + +**Paginación:** +- Leer `searchParams.page` (default 1). +- Query de responses: `.range((page-1)*50, page*50-1)` en vez de `.limit(500)`. +- Mostrar: "Página N de M · X respuestas totales". +- Links Anterior / Siguiente que modifican `?page=N` en la URL. +- Si hay menos de 50 resultados, no mostrar paginación. + +**Columna de acciones:** +Agregar columna "Ver" en la fila cabecera de cada submission (isFirstInGroup=true) +con link a `/admin/responses/[response_id]`. + +**Header informativo:** +Mostrar claramente: total de submissions, total de answers, survey seleccionado. + +### 4.2 Vista detallada — `app/admin/responses/[id]/page.tsx` + +Server Component. Ruta: `/admin/responses/[id]` donde `id` = `response_id` (uuid). + +**Query principal:** +```ts +// 1. Metadata de la respuesta +const { data: response } = await supabase + .from('responses') + .select('id, submitted_at, qi_zone, qi_sector, qi_age_bucket, survey_id') + .eq('id', id) + .single() + +// 2. Preguntas permitidas (mismo filtro que la lista) +const { data: questions } = await supabase + .from('questions') + .select('id, code, prompt, type, position, section_number, section_title') + .eq('is_sensitive', false) + .neq('type', 'text_long') + .eq('survey_id', response.survey_id) + .order('position') + +// 3. Answers de esta respuesta +const { data: answers } = await supabase + .from('answers') + .select('question_id, value, instance_id') + .eq('response_id', id) + .in('question_id', questions.map(q => q.id)) +``` + +**Layout:** +- Header: fecha, zona, sector, edad — datos de qi_* de la respuesta. +- Link "← Volver a la lista" que preserva el `?survey_id` y `?page` anteriores + (pasar como searchParam o usar `referrer`). +- Respuestas agrupadas por sección (section_number + section_title). +- Dentro de cada sección: tabla código | pregunta | valor. +- Instancias (instance_id != 'default'): mostrar agrupadas bajo "Familiar N" + con badge visual. +- Valores nulos: badge "sin respuesta" (igual que la lista). +- Si `id` no existe o RLS bloquea: mostrar "Respuesta no encontrada" — + no crashear. + +### 4.3 Tabla de respuestas — `app/admin/responses/responses-table.tsx` +Agregar la columna "Ver" en las filas isFirstInGroup con `` a +`/admin/responses/[submissionId]`. Pasar `surveyId` y `page` como props +para construir el link de "← Volver" en la vista detallada. + +## 5. DoD antes de reportar +- [ ] `git log --name-only -1` — listar todos los archivos tocados +- [ ] Filtro de survey carga desde DB, no hardcodeado +- [ ] Paginación funciona: page=1 muestra primeros 50, page=2 los siguientes +- [ ] Link "Ver" en cada submission lleva a `/admin/responses/[id]` +- [ ] Vista detallada muestra respuestas agrupadas por sección +- [ ] Instancias agrupadas bajo "Familiar N" +- [ ] RLS respetado: misma exclusión de sensibles que la lista +- [ ] "Respuesta no encontrada" si el id no existe +- [ ] Build sin errores: `npm run build` +- [ ] `git log --name-only -1` +- [ ] Commit + push a Gitea + +## 6. Qué reportar +``` +Verificado: +- Filtro survey: ✓ — query a tabla surveys, no hardcodeado +- Paginación: ✓ — page size 50, range correcto +- Link "Ver" → /admin/responses/[id]: ✓ +- Vista detallada: ✓ — agrupada por sección, instancias bajo "Familiar N" +- RLS/exclusión sensibles: ✓ — mismo filtro que antes +- "No encontrada" si id inválido: ✓ +- Build: ✓ +- git log --name-only -1: [lista completa] +- Commit: [hash] + +Decisiones de diseño tomadas: +- [lo que haya] + +Hallazgos: +- [lo que aparezca, especialmente sobre questions.section_number/section_title + — si esas columnas no existen, reportarlo antes de inventar una solución] +``` + +## 7. Qué NO hacer +- ❌ No tocar el filtro `is_sensitive=false` ni `type != 'text_long'` +- ❌ No usar service_role — solo getSupabaseSession() con RLS +- ❌ No agregar librerías de tablas o UI externas +- ❌ No implementar búsqueda por texto (fuera de alcance) +- ❌ No hacer deploy — solo commit y push a Gitea +- ❌ Si `section_number` o `section_title` no existen en questions, + reportarlo y NO inventar columnas — esperar instrucción + +## 8. Versionado +``` +feat(admin): vista respuestas mejorada + vista detallada [admin] + +/admin/responses: filtro survey dinámico, paginación 50/página, +link a vista detallada. /admin/responses/[id]: respuestas por sección, +instancias bajo "Familiar N". RLS y exclusión de sensibles intactos. +``` diff --git a/sprints/sprint-5/ETAPA_E2E_PROMPT.md b/sprints/sprint-5/ETAPA_E2E_PROMPT.md new file mode 100644 index 0000000..05ddb2f --- /dev/null +++ b/sprints/sprint-5/ETAPA_E2E_PROMPT.md @@ -0,0 +1,89 @@ +# ETAPA E2E — Tests Playwright: verificación deploy Sprint 5 + +## Antes de tocar código, leé: +1. `CLAUDE.md` +2. `docs/estado/ESTADO_PROYECTO_26JUN2026.md` — estado general del proyecto + +Confirmame el plan antes de implementar. + +## Credenciales de test (usuario dedicado — nunca las de Marcos) +- URL: `https://economia-cuidado.inqualityhq.com` +- Email: `test-e2e@inquality.com.py` +- Password: `TestE2E2026!#Cuidado` +- Rol: `platform_admin`, org: `InQuality` + +Estas credenciales van en `.env.test` en la raíz (agregar a `.gitignore` si no está). +**Nunca hardcodeadas en los specs.** + +## Verificar Playwright antes de instalar nada + +```bash +npx playwright --version 2>/dev/null && echo "instalado" || echo "no instalado" +ls ~/.cache/ms-playwright 2>/dev/null && echo "browsers presentes" || echo "browsers ausentes" +``` + +Reportar resultado antes de continuar. Si ya está instalado con browsers, agregar solo +`@playwright/test` como devDependency sin correr `npx playwright install`. +Si no está: `npx playwright install --with-deps chromium` (solo Chromium). + +## Los 5 tests a implementar + +**Archivo:** `tests/e2e/sprint5-verificacion.spec.ts` + +### Test 1 — Login y redirect +- Abrir `/admin/responses` sin sesión +- Verificar redirect a `/login` +- El DOM de `/login` contiene el campo `email`, el campo `password`, y el botón de toggle + de contraseña (buscar `aria-label` que contenga "contraseña") + +### Test 2 — Login funcional +- Hacer login con las credenciales de test en `/login` +- Verificar redirect a `/admin/responses` +- El DOM contiene `