fix(wizard): tests e2e sprint6 — cache geo, usabilidad, draft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
211
tests/e2e/sprint6-usabilidad.spec.ts
Normal file
211
tests/e2e/sprint6-usabilidad.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user