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