import { test, expect, type BrowserContext, type Page } from '@playwright/test' const DEMO_SURVEY_ID = '30000000-0000-0000-0000-000000000001' const SURVEY_URL = `/?s=${DEMO_SURVEY_ID}` // Devuelve el código de la pregunta visible (ej: '2.3'), o null si estamos en una sección. async function currentCode(page: Page): Promise { const el = page.locator('.wizard-prompt-code') if (await el.count() === 0) return null return (await el.textContent())?.trim() ?? null } // Navega hacia adelante hasta que la pregunta con `targetCode` sea visible. // Maneja preguntas required (Q1.1) y screens de sección intermedias. async function navigateUntil(page: Page, targetCode: string, maxSteps = 30): Promise { for (let i = 0; i < maxSteps; i++) { if (await currentCode(page) === targetCode) return // Si hay error de validación (required sin respuesta), responder primero if (await page.locator('.field-error').count() > 0) { await page.locator('.option-label').first().click() await page.waitForTimeout(150) continue } await page.getByRole('button', { name: 'Siguiente' }).click() // Esperar a que React aplique el state update y re-renderice await page.waitForTimeout(150) } const code = await currentCode(page) if (code !== targetCode) throw new Error(`navigateUntil: llegué a ${code}, esperaba ${targetCode}`) } // Acepta el consent y espera a que el wizard esté visible. async function acceptConsent(page: Page): Promise { await page.check('#consent-operativo') await page.getByRole('button', { name: 'Comenzar la encuesta' }).click() await page.locator('.wizard-shell').waitFor() } test.describe.serial('Sprint 6 — usabilidad y cache geo', () => { let context: BrowserContext let page: Page const geoRequests: string[] = [] test.beforeAll(async ({ browser }) => { context = await browser.newContext() page = await context.newPage() // Interceptar requests geo antes de navegar (para Test 1) page.on('request', (req) => { if (req.url().includes('geo_paraguay_full.json')) geoRequests.push(req.url()) }) }) test.afterAll(async () => { await context.close() }) // ───────────────────────────────────────────────────────────── // Test 1 — Cache geo: un solo fetch para barrio y ciudad // ───────────────────────────────────────────────────────────── test('1. cache geo: un solo fetch para barrio y ciudad', async () => { await page.goto(SURVEY_URL) await acceptConsent(page) // Navegar hasta Q2.2 (CiudadSelectField monta → geo request se dispara aquí) await navigateUntil(page, '2.2') expect(await currentCode(page)).toBe('2.2') // Avanzar a Q2.3 (BarrioAutocompleteField monta → debe usar cache, no nueva request) await page.getByRole('button', { name: 'Siguiente' }).click() await page.waitForTimeout(200) expect(await currentCode(page)).toBe('2.3') // Solo debe haber habido 1 request al JSON de geo (Q2.2 lo cargó, Q2.3 usó el cache) expect(geoRequests.length).toBe(1) }) // ───────────────────────────────────────────────────────────── // Test 2 — Estado loading del campo geo // ───────────────────────────────────────────────────────────── test('2. estado loading del campo geo', async () => { // Estamos en Q2.3 (del test anterior). Volver a Q2.2 para observar el estado del campo. await page.getByRole('button', { name: 'Anterior' }).click() await page.waitForTimeout(200) expect(await currentCode(page)).toBe('2.2') // CiudadSelectField: input con clase .text-short-input const input = page.locator('input.text-short-input').first() await expect(input).toBeVisible() const isDisabled = await input.isDisabled() if (isDisabled) { // Estado loading capturado — verificar placeholder correcto const placeholder = await input.getAttribute('placeholder') expect(placeholder).toBe('Cargando opciones...') } // En cualquier caso, el campo debe estar (o volverse) habilitado await expect(input).toBeEnabled({ timeout: 5000 }) // Volver a Q2.3 para el siguiente test await page.getByRole('button', { name: 'Siguiente' }).click() await page.waitForTimeout(200) expect(await currentCode(page)).toBe('2.3') }) // ───────────────────────────────────────────────────────────── // Test 3 — Texto libre funciona en campo barrio // ───────────────────────────────────────────────────────────── test('3. texto libre funciona en campo barrio', async () => { // Estamos en Q2.3 (BarrioAutocompleteField) expect(await currentCode(page)).toBe('2.3') const barrioCampo = page.locator('input.text-short-input') await expect(barrioCampo).toBeVisible() await expect(barrioCampo).toBeEnabled() // Escribir un barrio que no existe en el JSON (texto libre) await barrioCampo.fill('Barrio Inexistente XYZ') await expect(barrioCampo).toHaveValue('Barrio Inexistente XYZ') // Avanzar — campo es opcional, texto libre debe aceptarse sin error await page.getByRole('button', { name: 'Siguiente' }).click() await page.waitForTimeout(200) await expect(page.locator('.field-error')).toHaveCount(0) await expect(page.locator('.wizard-shell')).toBeVisible() }) // ───────────────────────────────────────────────────────────── // Test 4 — Draft persiste tras recarga // ───────────────────────────────────────────────────────────── test('4. draft persiste tras recarga', async () => { // Volver a Q2.3 para sobreescribir el draft con un valor reconocible await page.getByRole('button', { name: 'Anterior' }).click() await page.waitForTimeout(200) expect(await currentCode(page)).toBe('2.3') const barrioCampo = page.locator('input.text-short-input') await barrioCampo.fill('Barrio Test Draft') await expect(barrioCampo).toHaveValue('Barrio Test Draft') // Avanzar — handleNext guarda el draft inmediatamente (no debounce) await page.getByRole('button', { name: 'Siguiente' }).click() await page.waitForTimeout(300) // Verificar que el draft fue guardado antes de recargar const draftRaw = await page.evaluate( (id) => localStorage.getItem(`survey_draft_${id}`), DEMO_SURVEY_ID, ) expect(draftRaw, 'draft debe existir en localStorage antes del reload').not.toBeNull() expect(draftRaw, 'draft debe contener el valor del barrio').toContain('Barrio Test Draft') // Recargar await page.reload() // Verificar que el draft sobrevive el reload const draftAfterReload = await page.evaluate( (id) => localStorage.getItem(`survey_draft_${id}`), DEMO_SURVEY_ID, ) expect(draftAfterReload, 'draft debe persistir después del reload').not.toBeNull() await acceptConsent(page) // El indicador de restauración es el valor de Q2.3 — navegamos hasta ahí // (navigateUntil funciona desde cualquier pantalla de inicio, incluyendo S1) await navigateUntil(page, '2.3') await expect(page.locator('input.text-short-input')).toHaveValue('Barrio Test Draft') }) // ───────────────────────────────────────────────────────────── // Test 5 — Submit con error de red muestra estado error del wizard // ───────────────────────────────────────────────────────────── test('5. submit con error de red muestra estado error del wizard', async ({ browser }) => { const ctx5 = await browser.newContext() const page5 = await ctx5.newPage() // Pre-rellenar Q1.1 (única pregunta required) para no bloquearnos al avanzar await page5.goto(SURVEY_URL) await page5.evaluate((id) => { localStorage.setItem(`survey_draft_${id}`, JSON.stringify({ '1.1:default': 'true' })) }, DEMO_SURVEY_ID) await acceptConsent(page5) // Navegar hasta el end screen: todas las preguntas restantes son opcionales let iters = 0 while ( (await page5.locator('button', { hasText: 'Enviar encuesta' }).count()) === 0 && iters < 70 ) { await page5.getByRole('button', { name: 'Siguiente' }).click() await page5.waitForTimeout(100) iters++ } await expect(page5.getByRole('button', { name: 'Enviar encuesta' })).toBeVisible() // Poner el contexto offline para simular error de red en el submit await page5.context().setOffline(true) await page5.getByRole('button', { name: 'Enviar encuesta' }).click() // El wizard debe mostrar su propio estado de error (no un React error boundary) await expect(page5.locator('.error-container')).toBeVisible({ timeout: 15000 }) await expect(page5.getByText(/Application error/i)).toHaveCount(0) await expect(page5.getByRole('button', { name: /Volver e intentar/i })).toBeVisible() await page5.context().setOffline(false) await ctx5.close() }) })