diff --git a/.env.example b/.env.example index d96582b..c294917 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,9 @@ # Obtener desde Supabase Dashboard → Project Settings → API VITE_SUPABASE_URL=https://.supabase.co VITE_SUPABASE_ANON_KEY= + +# Usuario de test para E2E (email/password, NO Google OAuth) — crear en +# Supabase Studio → Authentication, y darle platform_role='platform_admin' +# en la tabla public.users. Va en .env.test (gitignored), no en .env.local. +TEST_USER_EMAIL=test@inquality.com.py +TEST_USER_PASSWORD= diff --git a/.gitignore b/.gitignore index 3ce60ec..ef66ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env.test # Editor directories and files .vscode/* @@ -26,6 +27,7 @@ dist-ssr # Playwright outputs tests/e2e/__output__/ test-results/ +tests/e2e/setup/auth-state.json # Wrangler .wrangler/ diff --git a/OBSERVATIONS.md b/OBSERVATIONS.md index f582fa5..1670fac 100644 --- a/OBSERVATIONS.md +++ b/OBSERVATIONS.md @@ -18,7 +18,13 @@ ## Hallazgos activos -*(vacío)* +### [DEFERRED-BACKLOG] Compartir proceso — implementación real diferida hasta primer guest + +**Etapa:** 5 (UI placeholder), decisión confirmada post-Etapa 7B +**Estado actual:** `setShared()` en `process-repo.ts` es no-op con comentario explicativo. El botón "Compartir con equipo" en LibraryPage muestra toast de confirmación pero no escribe en `process_access`. Todos los usuarios actuales son `platform_admin` y ven todo por bypass de RLS — el sharing no tiene efecto observable hoy. +**Schema listo:** tabla `process_access (process_id, grantee_type, grantee_id, permission)` + `user_groups` + `group_memberships` + RLS policies que ya usan `process_access` para usuarios no-admin. +**Trigger para implementar:** cuando se agregue el primer usuario `guest` (cliente externo o consultor externo) a la plataforma. +**Pendiente al implementar:** (1) UX de selección — ¿compartir con usuario específico, grupo, o "todo el equipo"? (2) implementación real de `setShared()` con escritura en `process_access`, (3) UI de gestión de grupos. --- diff --git a/docs/CHECKLIST_ENTREGA.md b/docs/CHECKLIST_ENTREGA.md new file mode 100644 index 0000000..f07a042 --- /dev/null +++ b/docs/CHECKLIST_ENTREGA.md @@ -0,0 +1,125 @@ +# Checklist de entrega — InQ ROI + +> Claude Code ejecuta este checklist ANTES de reportar una etapa como completada. +> El director ejecuta la sección "Validación visual" para el OK formal. + +--- + +## Tier 1 — Automatizado (bloquea entrega si falla) + +Correr en este orden. Si cualquier comando falla, no entregar — corregir primero. + +```bash +npm run build # 0 errores TypeScript, bundle generado +npm run lint # 0 errores ESLint +npm run test # todos los tests Vitest verdes (reportar N/N) +npx playwright test # todos los E2E verdes (reportar N/N) +``` + +Reportar en el mensaje de entrega: +- Build: ✅ limpio / ❌ N errores +- Lint: ✅ limpio / ❌ N warnings/errors +- Tests unitarios: ✅ N/N verdes +- Tests E2E: ✅ N/N verdes + +**Nota sobre E2E con auth:** los specs que requieren sesión usan `test.use({ storageState: 'tests/e2e/setup/auth-state.json' })`. Ese archivo lo genera el `globalSetup` (`tests/e2e/setup/auth-setup.ts`) automáticamente al correr `npx playwright test`, usando el usuario de test (`TEST_USER_EMAIL`/`TEST_USER_PASSWORD` en `.env.test`, gitignored). Si `.env.test` no existe, el setup avisa por consola y los specs con auth fallan — no es necesario ningún paso manual previo más allá de tener `.env.test` configurado una vez. + +--- + +## Tier 2 — Deploy check (post-deploy en inq-roi.inqualityhq.com) + +Abrir DevTools → Network tab. Realizar estas acciones y verificar: + +| Acción | Verificar | +|--------|-----------| +| Cargar biblioteca (`/`) | Sin 500, sin 406 en `simulations` | +| Abrir cualquier proceso en workspace | Sin 500, `activities` y `gateways` con 200 | +| Abrir catálogo de recursos (`/recursos`) | Sin 500, tabla carga con datos | +| Exportar PDF (cualquier proceso simulado) | Sin errores en consola, archivo descargado | + +--- + +## Tier 3 — Validación visual (director) + +> Esta sección la completa el director. Claude Code NO puede aprobarla. + +### Siempre verificar +- [ ] Header muestra avatar del usuario logueado +- [ ] Ningún texto dice "Process Cost Platform" (nombre viejo) +- [ ] Marca "InQ" escrita correctamente (no INQ, no Inq) +- [ ] Botones primarios en naranja `#F59845` + +### Por área (verificar si la etapa tocó esa área) + +**Biblioteca** +- [ ] Procesos cargan sin spinner infinito +- [ ] "Actualizado por [nombre]" visible en tarjetas +- [ ] Toggle Todos/Míos funciona + +**Workspace** +- [ ] BPMN canvas renderiza el diagrama +- [ ] ActivityPanel abre al hacer click en una actividad +- [ ] ResourcesPanel muestra recursos usados en el proceso + +**Catálogo de recursos** +- [ ] Tabla muestra recursos con badges de tipo coloreados +- [ ] Búsqueda por nombre funciona +- [ ] Filtro por tipo funciona +- [ ] Crear/editar recurso guarda en Supabase y aparece en la lista +- [ ] Recurso con costo `0.037` muestra `$0.037` (no `$0.04`) + +**Auth** +- [ ] Login muestra selector de cuentas de Google +- [ ] Logout redirige a `/login` +- [ ] Sin sesión, cualquier ruta protegida redirige a `/login` + +**PDF** (si la etapa tocó PDF) +- [ ] Footer dice "Powered by InQuality" +- [ ] Sin caracteres rotos ni encodings incorrectos + +--- + +## Tier 4 — Integridad de datos (si la etapa tocó schema o repos) + +Correr en Supabase Studio SQL Editor: + +```sql +-- Sin procesos huérfanos (sin owner) +SELECT COUNT(*) FROM processes WHERE owner_id IS NULL; -- debe ser 0 + +-- Sin actividades huérfanas +SELECT COUNT(*) FROM activities a +LEFT JOIN processes p ON p.id = a.process_id +WHERE p.id IS NULL; -- debe ser 0 + +-- Sin recursos con tipo inválido +SELECT DISTINCT type FROM resources; +-- debe mostrar solo: person, role, system, equipment, supply +``` + +--- + +## Nota sobre regresiones de PDF + +Los tests E2E de PDFs (`etapa-4-pdfs.spec.ts` etc.) son la red de contención para regresiones de PDF. +Si se tocó cualquier cosa de exportación o cálculos, correr específicamente: +```bash +npx playwright test tests/e2e/export-pdf.spec.ts tests/e2e/export-roi.spec.ts +``` + +--- + +## Setup del usuario de test E2E (referencia, una sola vez por entorno) + +1. Supabase Studio → Authentication → Add user (email/password, NO Google OAuth) +2. SQL Editor: + ```sql + insert into public.users (id, name, platform_role) + values ('', 'Nombre Test E2E', 'platform_admin'); + ``` +3. Completar `.env.test` (gitignored, ver `.env.example`): + ``` + TEST_USER_EMAIL=... + TEST_USER_PASSWORD=... + ``` +4. `npx playwright test` genera `tests/e2e/setup/auth-state.json` automáticamente en cada corrida (también gitignored). diff --git a/playwright.config.ts b/playwright.config.ts index b0ea3a2..62b4dc5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from '@playwright/test' export default defineConfig({ testDir: './tests/e2e', + testIgnore: '**/setup/**', + globalSetup: './tests/e2e/setup/auth-setup.ts', timeout: 180_000, use: { baseURL: 'http://localhost:4173', diff --git a/sprints/sprint-4/ETAPA_8_PROMPT.md b/sprints/sprint-4/ETAPA_8_PROMPT.md new file mode 100644 index 0000000..c290835 --- /dev/null +++ b/sprints/sprint-4/ETAPA_8_PROMPT.md @@ -0,0 +1,318 @@ +# Sprint 4 — Etapa 8: Tests de integración + batería de entrega + polish + +## Contexto + +InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. +Etapas 1–7B completadas. Sprint 4 introdujo auth (Google OAuth + Supabase), PostgreSQL, RLS, catálogo de recursos, y múltiples páginas nuevas. + +**Problema crítico detectado:** todos los tests E2E existentes (15 specs en `tests/e2e/`) corren contra `localhost:4173` sin autenticación. Con Sprint 4, todas las rutas protegidas redirigen a `/login`. Es muy probable que la mayoría de los specs existentes estén fallando o saltando pasos silenciosamente. + +Esta etapa tiene tres entregables: +1. **Auditoría y corrección** de los tests E2E existentes +2. **Nuevos tests E2E** para los flujos críticos de Sprint 4 +3. **`docs/CHECKLIST_ENTREGA.md`** — protocolo exhaustivo que Claude Code ejecuta antes de entregar cualquier etapa, en cualquier sprint futuro + +--- + +## Parte 1 — Auditoría de tests existentes + +### 1a. Correr todos los specs actuales y reportar estado + +```bash +npx playwright test --reporter=list 2>&1 | tail -50 +``` + +Clasificar cada spec como: +- **VERDE** — pasa sin cambios +- **ROJO-AUTH** — falla porque la app redirige a `/login` antes de que el test llegue al contenido +- **ROJO-OTRO** — falla por otra razón + +### 1b. Fix para specs ROJO-AUTH + +Los specs que prueban flujos con datos (PDFs, simulaciones, BPMN) necesitan un mecanismo de bypass de auth para tests. + +**Estrategia: mock de sesión vía localStorage en global setup** + +Crear `tests/e2e/setup/auth-setup.ts`: + +```typescript +import { chromium, FullConfig } from '@playwright/test' + +async function globalSetup(config: FullConfig) { + // Usar supabase-js directamente con service role para crear sesión de test + // O: inyectar una sesión pre-generada en localStorage antes de que el test navegue + + // Opción A (recomendada si hay usuario email/password en Supabase): + // const { data } = await supabase.auth.signInWithPassword({ + // email: process.env.TEST_USER_EMAIL, + // password: process.env.TEST_USER_PASSWORD, + // }) + // guardar storageState con la sesión + + // Opción B (mock mínimo para tests que no necesitan queries reales): + // Inyectar localStorage key de sesión fake para bypasear el guard de AuthContext +} +``` + +**Prerequisito para Opción A:** crear un usuario de test en Supabase con email/password (NO Google OAuth) con `platform_role = 'platform_admin'`. Las credenciales van en `.env.test` (ignorado por git) y documentadas en `.env.example`: + +``` +TEST_USER_EMAIL=test@inquality.com.py +TEST_USER_PASSWORD=... +``` + +**Si Opción A no es viable ahora:** los specs que prueban PDF/screenshots pueden mockear el store de Zustand directamente (los PDFs no necesitan Supabase para generarse — solo necesitan datos en el store). Revisar si se puede inicializar el store con fixtures sin pasar por la DB. + +Documentar la decisión tomada en un comentario al inicio del setup. + +### 1c. Specs que prueban solo PDFs/screenshots (sin data de Supabase) + +Los specs `etapa-4-pdfs.spec.ts` hasta `etapa-11-polish.spec.ts` y `export-pdf.spec.ts` probablemente cargan BPMNs locales y generan PDFs sin tocar Supabase. Para estos: + +- Si el único problema es el redirect a `/login`, agregar el mock de sesión del setup +- Si necesitan datos en el store, cargarlos via fixture antes de navegar + +No reescribir la lógica de los tests — solo agregar el mecanismo de auth bypass. + +--- + +## Parte 2 — Nuevos tests E2E para Sprint 4 + +Crear `tests/e2e/sprint-4-smoke.spec.ts`: + +### Tests sin auth (deben pasar sin sesión) + +```typescript +test('login page renders — Google button visible', async ({ page }) => { + await page.goto('/login') + await expect(page.getByRole('button', { name: /iniciar sesión con google/i })).toBeVisible() +}) + +test('protected route redirects to login', async ({ page }) => { + await page.goto('/') + await expect(page).toHaveURL(/\/login/) +}) + +test('protected route /workspace redirects to login', async ({ page }) => { + await page.goto('/workspace/fake-id') + await expect(page).toHaveURL(/\/login/) +}) + +test('protected route /recursos redirects to login', async ({ page }) => { + await page.goto('/recursos') + await expect(page).toHaveURL(/\/login/) +}) +``` + +### Tests con auth (requieren sesión de test) + +Usar el fixture de sesión del global setup: + +```typescript +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + +test('library loads after auth — no 500 errors', async ({ page }) => { + const errors: string[] = [] + page.on('response', r => { if (r.status() >= 500) errors.push(r.url()) }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + expect(errors).toHaveLength(0) +}) + +test('library — no 406 errors in simulations queries', async ({ page }) => { + const errors406: string[] = [] + page.on('response', r => { + if (r.status() === 406 && r.url().includes('simulations')) errors406.push(r.url()) + }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + expect(errors406).toHaveLength(0) +}) + +test('catalog page loads at /recursos', async ({ page }) => { + await page.goto('/recursos') + await expect(page.getByRole('heading', { name: /catálogo de recursos/i })).toBeVisible() +}) + +test('app header shows user avatar', async ({ page }) => { + await page.goto('/') + // El avatar tiene las iniciales del usuario o una imagen + await expect(page.locator('[aria-label*="usuario"]').or( + page.locator('img[alt*="avatar"]') + )).toBeVisible() +}) + +test('import BPMN — navigates to workspace', async ({ page }) => { + await page.goto('/import') + // Hacer click en "Usar ejemplo" si existe, o subir simple-linear.bpmn + // Verificar que navega al workspace + await expect(page).toHaveURL(/\/workspace\//) +}) +``` + +Agregar solo los tests que sean estables y no flaky. Si un flujo es difícil de automatizar (ej: el import con file upload real), documentarlo como "manual" en el checklist. + +--- + +## Parte 3 — `docs/CHECKLIST_ENTREGA.md` + +Crear este archivo. Es el protocolo que Claude Code ejecuta **antes de reportar cualquier etapa como completada**, en este sprint y en todos los futuros. + +```markdown +# Checklist de entrega — InQ ROI + +> Claude Code ejecuta este checklist ANTES de reportar una etapa como completada. +> El director ejecuta la sección "Validación visual" para el OK formal. + +--- + +## Tier 1 — Automatizado (bloquea entrega si falla) + +Correr en este orden. Si cualquier comando falla, no entregar — corregir primero. + +\`\`\`bash +npm run build # 0 errores TypeScript, bundle generado +npm run lint # 0 errores ESLint +npm run test # todos los tests Vitest verdes (reportar N/N) +npx playwright test # todos los E2E verdes (reportar N/N) +\`\`\` + +Reportar en el mensaje de entrega: +- Build: ✅ limpio / ❌ N errores +- Lint: ✅ limpio / ❌ N warnings/errors +- Tests unitarios: ✅ N/N verdes +- Tests E2E: ✅ N/N verdes + +--- + +## Tier 2 — Deploy check (post-deploy en inq-roi.inqualityhq.com) + +Abrir DevTools → Network tab. Realizar estas acciones y verificar: + +| Acción | Verificar | +|--------|-----------| +| Cargar biblioteca (`/`) | Sin 500, sin 406 en `simulations` | +| Abrir cualquier proceso en workspace | Sin 500, `activities` y `gateways` con 200 | +| Abrir catálogo de recursos (`/recursos`) | Sin 500, tabla carga con datos | +| Exportar PDF (cualquier proceso simulado) | Sin errores en consola, archivo descargado | + +--- + +## Tier 3 — Validación visual (director) + +> Esta sección la completa el director. Claude Code NO puede aprobarla. + +### Siempre verificar +- [ ] Header muestra avatar del usuario logueado +- [ ] Ningún texto dice "Process Cost Platform" (nombre viejo) +- [ ] Marca "InQ" escrita correctamente (no INQ, no Inq) +- [ ] Botones primarios en naranja `#F59845` + +### Por área (verificar si la etapa tocó esa área) + +**Biblioteca** +- [ ] Procesos cargan sin spinner infinito +- [ ] "Actualizado por [nombre]" visible en tarjetas +- [ ] Toggle Todos/Míos funciona + +**Workspace** +- [ ] BPMN canvas renderiza el diagrama +- [ ] ActivityPanel abre al hacer click en una actividad +- [ ] ResourcesPanel muestra recursos usados en el proceso + +**Catálogo de recursos** +- [ ] Tabla muestra recursos con badges de tipo coloreados +- [ ] Búsqueda por nombre funciona +- [ ] Filtro por tipo funciona +- [ ] Crear/editar recurso guarda en Supabase y aparece en la lista +- [ ] VPS con costo `0.037` muestra `$0.037` (no `$0.04`) + +**Auth** +- [ ] Login muestra selector de cuentas de Google +- [ ] Logout redirige a `/login` +- [ ] Sin sesión, cualquier ruta protegida redirige a `/login` + +**PDF** (si la etapa tocó PDF) +- [ ] Footer dice "Powered by InQuality" +- [ ] Sin caracteres rotos ni encodings incorrectos + +--- + +## Tier 4 — Integridad de datos (si la etapa tocó schema o repos) + +Correr en Supabase Studio SQL Editor: + +\`\`\`sql +-- Sin procesos huérfanos (sin owner) +SELECT COUNT(*) FROM processes WHERE owner_id IS NULL; -- debe ser 0 + +-- Sin actividades huérfanas +SELECT COUNT(*) FROM activities a +LEFT JOIN processes p ON p.id = a.process_id +WHERE p.id IS NULL; -- debe ser 0 + +-- Sin recursos con tipo inválido +SELECT DISTINCT type FROM resources; +-- debe mostrar solo: person, role, system, equipment, supply +\`\`\` + +--- + +## Nota sobre regresiones de PDF + +Los tests E2E de PDFs (`etapa-4-pdfs.spec.ts` etc.) son la red de contención para regresiones de PDF. +Si se tocó cualquier cosa de exportación o cálculos, correr específicamente: +\`\`\`bash +npx playwright test tests/e2e/export-pdf.spec.ts tests/e2e/export-roi.spec.ts +\`\`\` +``` + +--- + +## Parte 4 — Polish + +Revisar y corregir cualquiera de estos si existe: + +- En la biblioteca, si el `OwnershipBadge` todavía tiene texto "Solo vos" / "De otro consultor" en inglés o hardcodeado — corregir a español +- Verificar que el texto de "Compartir con equipo" en el menú ⋮ sigue siendo visible aunque sea no-op (con un tooltip o texto secundario que diga "Disponible próximamente" si el director lo prefiere — **consultar antes de implementar**) +- Revisar que `npm run lint` pasa sin warnings en los archivos nuevos de Sprint 4 + +--- + +## Qué NO entra en esta etapa + +- Implementación real de `setShared` (diferida — ver OBSERVATIONS.md) +- Keycloak / migración de auth +- Features de producto nuevas +- Cambios en PDF o simulación + +--- + +## Orden de ejecución + +1. Correr todos los E2E existentes y clasificar estado (verde/rojo-auth/rojo-otro) +2. Implementar estrategia de auth bypass para tests (Opción A o B según viabilidad) +3. Corregir specs ROJO-AUTH con el nuevo setup +4. Crear `tests/e2e/sprint-4-smoke.spec.ts` +5. Crear `docs/CHECKLIST_ENTREGA.md` +6. Polish (lint warnings, textos) +7. `npm run test` — mantener 521+ tests Vitest verdes +8. `npx playwright test` — reportar estado completo (verde/rojo con razón) +9. Commit: `test(sprint-4/etapa-8): batería E2E Sprint 4 + CHECKLIST_ENTREGA + polish` + +--- + +## Criterios de validación + +- [ ] Todos los specs E2E clasificados como verde, rojo-auth, o rojo-otro +- [ ] Specs ROJO-AUTH corregidos con estrategia de auth bypass documentada +- [ ] `tests/e2e/sprint-4-smoke.spec.ts` creado con al menos los 4 tests sin-auth +- [ ] `docs/CHECKLIST_ENTREGA.md` creado con las 4 secciones (Tier 1–4) +- [ ] `npm run lint` sin warnings en archivos de Sprint 4 +- [ ] 521+ tests Vitest verdes +- [ ] E2E: reportar N verdes / M rojos con razón para cada rojo +- [ ] **Validación visual del director** antes del OK formal diff --git a/src/features/resources/ResourceCatalogPage.tsx b/src/features/resources/ResourceCatalogPage.tsx index 361be0c..8345d55 100644 --- a/src/features/resources/ResourceCatalogPage.tsx +++ b/src/features/resources/ResourceCatalogPage.tsx @@ -12,7 +12,8 @@ import { useAuth } from '@/auth/AuthContext' import { supabaseResourceRepo, type ResourceWithCreator } from '@/persistence/supabase/resource-repo' import { supabaseProcessRepo } from '@/persistence/supabase/process-repo' import { formatCostPerHour } from '@/lib/format' -import { RESOURCE_TYPE_LABELS, RESOURCE_TYPE_STYLES, ResourceTypeAvatar } from './resource-display' +import { RESOURCE_TYPE_LABELS, RESOURCE_TYPE_STYLES } from './resource-types' +import { ResourceTypeAvatar } from './resource-display' import { ResourceFormSheet } from './ResourceFormSheet' import type { Resource, ResourceType } from '@/domain/types' diff --git a/src/features/resources/ResourceFormSheet.tsx b/src/features/resources/ResourceFormSheet.tsx index 9ab6c6d..78037b4 100644 --- a/src/features/resources/ResourceFormSheet.tsx +++ b/src/features/resources/ResourceFormSheet.tsx @@ -6,7 +6,7 @@ import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { RESOURCE_TYPE_LABELS } from './resource-display' +import { RESOURCE_TYPE_LABELS } from './resource-types' import type { Resource, ResourceType } from '@/domain/types' interface ResourceFormState { diff --git a/src/features/resources/resource-display.tsx b/src/features/resources/resource-display.tsx index 9c6bdc5..28c7ce5 100644 --- a/src/features/resources/resource-display.tsx +++ b/src/features/resources/resource-display.tsx @@ -1,28 +1,6 @@ -import { User, Briefcase, Server, Wrench, Package, type LucideIcon } from 'lucide-react' +import { RESOURCE_TYPE_STYLES } from './resource-types' import type { ResourceType } from '@/domain/types' -export const RESOURCE_TYPE_LABELS: Record = { - person: 'Persona', - role: 'Rol', - system: 'Sistema', - equipment: 'Equipo', - supply: 'Insumo', -} - -interface ResourceTypeStyle { - icon: LucideIcon - bg: string - fg: string -} - -export const RESOURCE_TYPE_STYLES: Record = { - person: { icon: User, bg: '#FEF3E7', fg: '#F59845' }, - role: { icon: Briefcase, bg: '#FEF3E7', fg: '#D97706' }, - system: { icon: Server, bg: '#EEF2FF', fg: '#6366F1' }, - equipment: { icon: Wrench, bg: '#F0FDF4', fg: '#16A34A' }, - supply: { icon: Package, bg: '#F1F5F9', fg: '#64748B' }, -} - export function ResourceTypeAvatar({ type, size = 32 }: { type: ResourceType; size?: number }) { const { icon: Icon, bg, fg } = RESOURCE_TYPE_STYLES[type] return ( diff --git a/src/features/resources/resource-types.ts b/src/features/resources/resource-types.ts new file mode 100644 index 0000000..d0696e6 --- /dev/null +++ b/src/features/resources/resource-types.ts @@ -0,0 +1,24 @@ +import { User, Briefcase, Server, Wrench, Package, type LucideIcon } from 'lucide-react' +import type { ResourceType } from '@/domain/types' + +export const RESOURCE_TYPE_LABELS: Record = { + person: 'Persona', + role: 'Rol', + system: 'Sistema', + equipment: 'Equipo', + supply: 'Insumo', +} + +export interface ResourceTypeStyle { + icon: LucideIcon + bg: string + fg: string +} + +export const RESOURCE_TYPE_STYLES: Record = { + person: { icon: User, bg: '#FEF3E7', fg: '#F59845' }, + role: { icon: Briefcase, bg: '#FEF3E7', fg: '#D97706' }, + system: { icon: Server, bg: '#EEF2FF', fg: '#6366F1' }, + equipment: { icon: Wrench, bg: '#F0FDF4', fg: '#16A34A' }, + supply: { icon: Package, bg: '#F1F5F9', fg: '#64748B' }, +} diff --git a/src/features/workspace/ResourcesPanel.tsx b/src/features/workspace/ResourcesPanel.tsx index 63f027f..1297cd6 100644 --- a/src/features/workspace/ResourcesPanel.tsx +++ b/src/features/workspace/ResourcesPanel.tsx @@ -6,7 +6,8 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { useProcessStore } from '@/store/process-store' import { useAuth } from '@/auth/AuthContext' import { formatCostPerHour } from '@/lib/format' -import { RESOURCE_TYPE_LABELS, ResourceTypeAvatar } from '@/features/resources/resource-display' +import { RESOURCE_TYPE_LABELS } from '@/features/resources/resource-types' +import { ResourceTypeAvatar } from '@/features/resources/resource-display' import { ResourceFormSheet } from '@/features/resources/ResourceFormSheet' import type { Resource } from '@/domain/types' diff --git a/src/persistence/supabase/process-repo.ts b/src/persistence/supabase/process-repo.ts index 29fe31b..a81b91b 100644 --- a/src/persistence/supabase/process-repo.ts +++ b/src/persistence/supabase/process-repo.ts @@ -102,6 +102,7 @@ export const supabaseProcessRepo = { // No-op en Sprint 4: la implementación real (insertar en process_access) llega cuando existan guests. // El método existe para que la UI de "Compartir con equipo" funcione sin escritura a DB. + // eslint-disable-next-line @typescript-eslint/no-unused-vars async setShared(_processId: string, _shared: boolean): Promise { return }, diff --git a/tests/e2e/etapa-11-polish.spec.ts b/tests/e2e/etapa-11-polish.spec.ts index 6b01e80..fcc5649 100644 --- a/tests/e2e/etapa-11-polish.spec.ts +++ b/tests/e2e/etapa-11-polish.spec.ts @@ -9,6 +9,8 @@ import { fileURLToPath } from 'url' import { PDFParse } from 'pdf-parse' import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, './__output__') @@ -35,7 +37,7 @@ async function getOrientations(buffer: Buffer): Promise { } test('etapa-11-roi.pdf — polish final página 1 ROI', async ({ page }) => { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/etapa-4-pdfs.spec.ts b/tests/e2e/etapa-4-pdfs.spec.ts index ed9a8ee..5b02828 100644 --- a/tests/e2e/etapa-4-pdfs.spec.ts +++ b/tests/e2e/etapa-4-pdfs.spec.ts @@ -13,6 +13,8 @@ import { mkdirSync, readFileSync } from 'fs' import { fileURLToPath } from 'url' import { PDFParse } from 'pdf-parse' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, './__output__') @@ -22,7 +24,7 @@ test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) }) // ─── Helpers ────────────────────────────────────────────────────────────────── async function importBpmn(page: import('@playwright/test').Page, filename: string) { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename)) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/etapa-5-pdfs.spec.ts b/tests/e2e/etapa-5-pdfs.spec.ts index cc63393..f379059 100644 --- a/tests/e2e/etapa-5-pdfs.spec.ts +++ b/tests/e2e/etapa-5-pdfs.spec.ts @@ -16,6 +16,8 @@ import { fileURLToPath } from 'url' import { PDFParse } from 'pdf-parse' import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, './__output__') @@ -44,7 +46,7 @@ async function getPageOrientations(buffer: Buffer): Promise { mkdirSync(SCREENSHOTS_DIR, { recursive: true }) }) async function loadProcess(page: import('@playwright/test').Page, filename: string) { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename)) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/export-pdf.spec.ts b/tests/e2e/export-pdf.spec.ts index 5f2a775..d96f139 100644 --- a/tests/e2e/export-pdf.spec.ts +++ b/tests/e2e/export-pdf.spec.ts @@ -14,6 +14,8 @@ import { PDFParse } from 'pdf-parse' import sharp from 'sharp' import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUTPUT_DIR = resolve(__dirname, '__output__') @@ -34,7 +36,7 @@ async function parsePdf(buffer: Buffer): Promise<{ numpages: number; text: strin // ─── Helper: importa un BPMN, simula y llega al reporte ────────────────────── async function importAndSimulate(page: import('@playwright/test').Page, bpmnFile: string) { - await page.goto('/') + await page.goto('/import') // Subir el BPMN vía el input oculto (equivalente a drag-and-drop programático) const fileInput = page.locator('#bpmn-file-input') @@ -308,7 +310,7 @@ test('Heatmap con gradiente real — medium-with-gateways.bpmn', async ({ page } } // ── 1. Importar BPMN y navegar al workspace ───────────────────────────────── - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) await page.waitForURL(/\/workspace\//) diff --git a/tests/e2e/export-roi.spec.ts b/tests/e2e/export-roi.spec.ts index 035431e..f9d96e2 100644 --- a/tests/e2e/export-roi.spec.ts +++ b/tests/e2e/export-roi.spec.ts @@ -21,6 +21,8 @@ import { resolve } from 'path' import { fileURLToPath } from 'url' import { PDFParse } from 'pdf-parse' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUTPUT_DIR = resolve(__dirname, '__output__') @@ -39,7 +41,7 @@ async function parsePdf(buffer: Buffer): Promise<{ numpages: number; text: strin // ─── Helper: importar BPMN y llegar al workspace ────────────────────────────── async function importBpmn(page: import('@playwright/test').Page, bpmnFile: string) { - await page.goto('/') + await page.goto('/import') const fileInput = page.locator('#bpmn-file-input') await fileInput.setInputFiles(resolve(BPMN_DIR, bpmnFile)) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) diff --git a/tests/e2e/payback-etapa-2.spec.ts b/tests/e2e/payback-etapa-2.spec.ts index e6888b2..0bda3e2 100644 --- a/tests/e2e/payback-etapa-2.spec.ts +++ b/tests/e2e/payback-etapa-2.spec.ts @@ -17,6 +17,8 @@ import { resolve } from 'path' import { mkdirSync } from 'fs' import { fileURLToPath } from 'url' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, './__output__') @@ -26,7 +28,7 @@ test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) }) // ─── Helpers ────────────────────────────────────────────────────────────────── async function importBpmn(page: import('@playwright/test').Page, filename: string) { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename)) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/prod-smoke.spec.ts b/tests/e2e/prod-smoke.spec.ts index 0588894..ce2bcdc 100644 --- a/tests/e2e/prod-smoke.spec.ts +++ b/tests/e2e/prod-smoke.spec.ts @@ -9,6 +9,8 @@ import { readFileSync, mkdirSync } from 'fs' import { resolve } from 'path' import { fileURLToPath } from 'url' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const OUTPUT_DIR = resolve(__dirname, '__output__') @@ -17,15 +19,12 @@ test.beforeAll(() => { }) test('prod-smoke — import → simulate → export PDF', async ({ page }) => { - // ── 1. Landing page carga correctamente ────────────────────────────────── - await page.goto('/') + // ── 1. Página de import carga correctamente (Sprint 4: requiere sesión) ─── + await page.goto('/import') await expect(page).toHaveTitle(/InQ ROI/) - // Verificar heading principal - await expect(page.getByText('Cuantificá el costo operativo')).toBeVisible({ timeout: 10_000 }) - // Verificar que los 3 procesos de ejemplo están presentes - await expect(page.getByText('Aprobación de crédito')).toBeVisible() + await expect(page.getByText('Aprobación de crédito')).toBeVisible({ timeout: 10_000 }) await expect(page.getByText('Proceso lineal simple')).toBeVisible() await expect(page.getByText('Control de calidad con revisión')).toBeVisible() diff --git a/tests/e2e/screenshots-etapa-1.spec.ts b/tests/e2e/screenshots-etapa-1.spec.ts index 6208f0d..d0a2d13 100644 --- a/tests/e2e/screenshots-etapa-1.spec.ts +++ b/tests/e2e/screenshots-etapa-1.spec.ts @@ -14,6 +14,8 @@ import { resolve } from 'path' import { mkdirSync } from 'fs' import { fileURLToPath } from 'url' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, '../screenshots/etapa-1') @@ -21,7 +23,7 @@ const OUT_DIR = resolve(__dirname, '../screenshots/etapa-1') test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) }) test('screenshot workspace — CTAs naranjas visibles', async ({ page }) => { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'simple-linear.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) @@ -31,7 +33,7 @@ test('screenshot workspace — CTAs naranjas visibles', async ({ page }) => { }) test('screenshot switch ON — switch de automatización naranja', async ({ page }) => { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'simple-linear.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) @@ -50,7 +52,7 @@ test('screenshot switch ON — switch de automatización naranja', async ({ page }) test('screenshot report-tabs — tab activo naranja', async ({ page }) => { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/screenshots-etapa-3.spec.ts b/tests/e2e/screenshots-etapa-3.spec.ts index d762e22..b041701 100644 --- a/tests/e2e/screenshots-etapa-3.spec.ts +++ b/tests/e2e/screenshots-etapa-3.spec.ts @@ -16,6 +16,8 @@ import { resolve } from 'path' import { mkdirSync } from 'fs' import { fileURLToPath } from 'url' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, '../screenshots/etapa-3') @@ -25,7 +27,7 @@ test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) }) // ─── Helpers ────────────────────────────────────────────────────────────────── async function importBpmn(page: import('@playwright/test').Page, filename: string) { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename)) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/setup/auth-setup.ts b/tests/e2e/setup/auth-setup.ts new file mode 100644 index 0000000..93c7a04 --- /dev/null +++ b/tests/e2e/setup/auth-setup.ts @@ -0,0 +1,83 @@ +// Global setup de Playwright — Sprint 4 Etapa 8. +// +// Decisión tomada: Opción A (sesión real contra Supabase con usuario de test +// email/password, platform_role='platform_admin' en public.users). Permite que +// los specs E2E ejerciten el flujo real de auth + RLS + queries, no un mock. +// +// Usuario de test creado manualmente en Supabase Studio (no vía signup automático). +// Credenciales en .env.test (gitignored) — ver .env.example para el formato. +// +// Genera tests/e2e/setup/auth-state.json: storageState de Playwright con la +// sesión inyectada en localStorage bajo la clave sb-{project_ref}-auth-token, +// el mismo formato que usa supabase-js v2 y que AuthContext.getUserFromStorage() +// espera leer. Los specs que necesitan auth usan: +// test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + +import { chromium } from '@playwright/test' +import { createClient } from '@supabase/supabase-js' +import { readFileSync, existsSync } from 'fs' +import { resolve } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const ROOT = resolve(__dirname, '../../..') +const STATE_PATH = resolve(__dirname, 'auth-state.json') + +function loadEnvFile(filename: string): Record { + const path = resolve(ROOT, filename) + if (!existsSync(path)) return {} + const out: Record = {} + for (const line of readFileSync(path, 'utf-8').split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const idx = trimmed.indexOf('=') + if (idx === -1) continue + out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1) + } + return out +} + +async function globalSetup() { + const local = loadEnvFile('.env.local') + const test = loadEnvFile('.env.test') + + const supabaseUrl = local.VITE_SUPABASE_URL + const supabaseAnonKey = local.VITE_SUPABASE_ANON_KEY + const email = test.TEST_USER_EMAIL + const password = test.TEST_USER_PASSWORD + + if (!supabaseUrl || !supabaseAnonKey) { + console.warn('[auth-setup] VITE_SUPABASE_URL/ANON_KEY no encontradas en .env.local — specs con auth se saltarán storageState') + return + } + if (!email || !password) { + console.warn('[auth-setup] TEST_USER_EMAIL/PASSWORD no encontradas en .env.test — specs con auth se saltarán storageState') + return + } + + const supabase = createClient(supabaseUrl, supabaseAnonKey) + const { data, error } = await supabase.auth.signInWithPassword({ email, password }) + if (error || !data.session) { + console.error('[auth-setup] Login de usuario de test falló:', error?.message) + return + } + + const projectRef = new URL(supabaseUrl).hostname.split('.')[0] + const storageKey = `sb-${projectRef}-auth-token` + + const browser = await chromium.launch() + const page = await browser.newPage() + await page.goto('http://localhost:4173/login') + await page.evaluate( + ([key, session]) => { + window.localStorage.setItem(key, JSON.stringify(session)) + }, + [storageKey, data.session] as const + ) + await page.context().storageState({ path: STATE_PATH }) + await browser.close() + + console.log(`[auth-setup] Sesión de test guardada en ${STATE_PATH}`) +} + +export default globalSetup diff --git a/tests/e2e/sprint-1-5-final.spec.ts b/tests/e2e/sprint-1-5-final.spec.ts index abb79a7..2c989f5 100644 --- a/tests/e2e/sprint-1-5-final.spec.ts +++ b/tests/e2e/sprint-1-5-final.spec.ts @@ -10,6 +10,8 @@ import { fileURLToPath } from 'url' import { PDFParse } from 'pdf-parse' import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUT_DIR = resolve(__dirname, './__output__/sprint-1-5-final') @@ -36,7 +38,7 @@ async function getOrientations(buffer: Buffer): Promise { } async function setupFullScenario(page: import('@playwright/test').Page) { - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) diff --git a/tests/e2e/sprint-4-smoke.spec.ts b/tests/e2e/sprint-4-smoke.spec.ts new file mode 100644 index 0000000..32e4a56 --- /dev/null +++ b/tests/e2e/sprint-4-smoke.spec.ts @@ -0,0 +1,77 @@ +/** + * Sprint 4 — Etapa 8: smoke tests de los flujos críticos introducidos por el sprint + * (auth, biblioteca con Supabase, catálogo de recursos). + * + * Los tests "sin auth" verifican el guard de rutas protegidas — corren sin storageState. + * Los tests "con auth" usan la sesión real generada por tests/e2e/setup/auth-setup.ts. + */ +import { test, expect } from '@playwright/test' + +// ─── Sin auth — deben pasar sin sesión ──────────────────────────────────────── + +test.describe('Sin sesión', () => { + test('login page renders — botón de Google visible', async ({ page }) => { + await page.goto('/login') + await expect(page.getByRole('button', { name: /iniciar sesión con google/i })).toBeVisible() + }) + + test('protected route / redirects to login', async ({ page }) => { + await page.goto('/') + await expect(page).toHaveURL(/\/login/) + }) + + test('protected route /workspace redirects to login', async ({ page }) => { + await page.goto('/workspace/fake-id') + await expect(page).toHaveURL(/\/login/) + }) + + test('protected route /recursos redirects to login', async ({ page }) => { + await page.goto('/recursos') + await expect(page).toHaveURL(/\/login/) + }) +}) + +// ─── Con auth — requieren sesión de test ────────────────────────────────────── + +test.describe('Con sesión', () => { + test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + + test('library loads after auth — sin errores 500', async ({ page }) => { + const errors: string[] = [] + page.on('response', (r) => { if (r.status() >= 500) errors.push(r.url()) }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + expect(errors).toHaveLength(0) + }) + + test('library — sin errores 406 en queries de simulations', async ({ page }) => { + const errors406: string[] = [] + page.on('response', (r) => { + if (r.status() === 406 && r.url().includes('simulations')) errors406.push(r.url()) + }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + expect(errors406).toHaveLength(0) + }) + + test('catalog page loads at /recursos', async ({ page }) => { + await page.goto('/recursos') + await expect(page.getByRole('heading', { name: /catálogo de recursos/i })).toBeVisible() + }) + + test('app header shows user avatar', async ({ page }) => { + await page.goto('/') + // El trigger del dropdown de usuario tiene title={user.name} (ver AppHeader.tsx) + await expect(page.locator('header button[title]').first()).toBeVisible() + }) + + test('import BPMN — navega al workspace', async ({ page }) => { + await page.goto('/import') + await page.getByText('Proceso lineal simple').click() + await expect(page).toHaveURL(/\/workspace\//, { timeout: 15_000 }) + }) +}) diff --git a/tests/e2e/validate-dod-etapa4.spec.ts b/tests/e2e/validate-dod-etapa4.spec.ts index 5c990a3..7843426 100644 --- a/tests/e2e/validate-dod-etapa4.spec.ts +++ b/tests/e2e/validate-dod-etapa4.spec.ts @@ -22,6 +22,8 @@ import { PDFParse } from 'pdf-parse' import sharp from 'sharp' import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib' +test.use({ storageState: 'tests/e2e/setup/auth-state.json' }) + const __dirname = fileURLToPath(new URL('.', import.meta.url)) const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') const OUTPUT_DIR = resolve(__dirname, '__output__') @@ -109,7 +111,7 @@ const AUTO_TIME = 5 test('DoD Etapa 4 — genera y valida los 6 archivos de export para medium-with-gateways', async ({ page }) => { // ── Fase 1: importar BPMN e inyectar configuración en IndexedDB ───────────── - await page.goto('/') + await page.goto('/import') await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) const processId = page.url().split('/workspace/')[1] diff --git a/tests/screenshots/etapa-1/report-tabs.png b/tests/screenshots/etapa-1/report-tabs.png index afb7a2b..bcdd8bd 100644 Binary files a/tests/screenshots/etapa-1/report-tabs.png and b/tests/screenshots/etapa-1/report-tabs.png differ diff --git a/tests/screenshots/etapa-1/switch-on.png b/tests/screenshots/etapa-1/switch-on.png index 68c59ba..ae19f3c 100644 Binary files a/tests/screenshots/etapa-1/switch-on.png and b/tests/screenshots/etapa-1/switch-on.png differ diff --git a/tests/screenshots/etapa-1/workspace.png b/tests/screenshots/etapa-1/workspace.png index 9230b40..7bff5ca 100644 Binary files a/tests/screenshots/etapa-1/workspace.png and b/tests/screenshots/etapa-1/workspace.png differ diff --git a/tests/screenshots/etapa-3/e3-comparison-table.png b/tests/screenshots/etapa-3/e3-comparison-table.png index bdf2a48..b553e96 100644 Binary files a/tests/screenshots/etapa-3/e3-comparison-table.png and b/tests/screenshots/etapa-3/e3-comparison-table.png differ diff --git a/tests/screenshots/etapa-3/e3-cumulative-chart.png b/tests/screenshots/etapa-3/e3-cumulative-chart.png index 2705eee..8511f51 100644 Binary files a/tests/screenshots/etapa-3/e3-cumulative-chart.png and b/tests/screenshots/etapa-3/e3-cumulative-chart.png differ diff --git a/tests/screenshots/etapa-3/e3-donut-chart.png b/tests/screenshots/etapa-3/e3-donut-chart.png index ade10ac..cfeeaec 100644 Binary files a/tests/screenshots/etapa-3/e3-donut-chart.png and b/tests/screenshots/etapa-3/e3-donut-chart.png differ diff --git a/tests/screenshots/etapa-3/e3-report-tabs.png b/tests/screenshots/etapa-3/e3-report-tabs.png index ade10ac..cfeeaec 100644 Binary files a/tests/screenshots/etapa-3/e3-report-tabs.png and b/tests/screenshots/etapa-3/e3-report-tabs.png differ diff --git a/tests/screenshots/etapa-3/e3-roi-kpi-hero.png b/tests/screenshots/etapa-3/e3-roi-kpi-hero.png index b22c39e..ca83af7 100644 Binary files a/tests/screenshots/etapa-3/e3-roi-kpi-hero.png and b/tests/screenshots/etapa-3/e3-roi-kpi-hero.png differ diff --git a/tests/screenshots/etapa-3/e3-switch-on.png b/tests/screenshots/etapa-3/e3-switch-on.png index 68c59ba..ae19f3c 100644 Binary files a/tests/screenshots/etapa-3/e3-switch-on.png and b/tests/screenshots/etapa-3/e3-switch-on.png differ diff --git a/tests/screenshots/etapa-3/e3-transparency.png b/tests/screenshots/etapa-3/e3-transparency.png index 086e215..759c693 100644 Binary files a/tests/screenshots/etapa-3/e3-transparency.png and b/tests/screenshots/etapa-3/e3-transparency.png differ diff --git a/tests/screenshots/etapa-3/e3-workspace.png b/tests/screenshots/etapa-3/e3-workspace.png index 9230b40..7bff5ca 100644 Binary files a/tests/screenshots/etapa-3/e3-workspace.png and b/tests/screenshots/etapa-3/e3-workspace.png differ diff --git a/tests/screenshots/etapa-9/workspace-badge-detail.png b/tests/screenshots/etapa-9/workspace-badge-detail.png index a13379d..dc1b548 100644 Binary files a/tests/screenshots/etapa-9/workspace-badge-detail.png and b/tests/screenshots/etapa-9/workspace-badge-detail.png differ diff --git a/tests/screenshots/etapa-9/workspace-badge-on.png b/tests/screenshots/etapa-9/workspace-badge-on.png index edd9bed..b721e71 100644 Binary files a/tests/screenshots/etapa-9/workspace-badge-on.png and b/tests/screenshots/etapa-9/workspace-badge-on.png differ