fix(wizard): tests e2e sprint6 — cache geo, usabilidad, draft

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
markosbenitez
2026-06-29 09:03:49 -03:00
parent 704d2407e8
commit e29752644a

View File

@@ -0,0 +1,211 @@
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<string | null> {
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<void> {
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<void> {
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()
})
})