// ===================================================================== // Verificación del portal en WebKit // Comprueba lo que la demo tiene que sostener: Magic Login, permisos, // que el contenido sea el del archivo de oferta educativa, que dirección // pueda editar de verdad, y que nada se desborde en ningún tamaño. // ===================================================================== import { test, expect } from '@playwright/test'; const ROLES = { director: { email: 'director@insta.demo', nombre: 'Marisol Aguilar', secciones: 10 }, administrativo: { email: 'administracion@insta.demo', nombre: 'Rubén Cázares', secciones: 9 }, empleado: { email: 'asesor@insta.demo', nombre: 'Denisse Ochoa', secciones: 8 }, }; const SECCIONES = ['inicio', 'oferta', 'requisitos', 'becas', 'clases', 'preguntas', 'mensajes', 'contacto', 'equipo', 'bitacora']; /** Entra por la vía real: genera el enlace y lo canjea, como haría el correo. */ async function entrar(page, email) { const r = await page.request.post('/api/auth/enlace', { data: { email } }); expect(r.ok(), `no se pudo generar el enlace de ${email}`).toBeTruthy(); const { token } = await r.json(); await page.goto(`/acceso/${token}`); await expect(page.locator('.barra__nav .nav-item').first()).toBeVisible(); } async function irA(page, seccion) { await page.goto(`/portal#/${seccion}`); await expect(page.locator('.cabecera-seccion h1')).toBeVisible(); await expect(page.locator('.cargando')).toHaveCount(0); // En teléfono los bloques largos llegan plegados. Se abren para que las // pruebas midan lo mismo en cualquier tamaño; el plegado se prueba aparte. await page.evaluate(() => { document.querySelectorAll('details.plegable').forEach((d) => { d.open = true; }); }); } // --------------------------------------------------------------------- test.describe('Entrada', () => { test('ofrece los tres accesos y explica el enlace de un solo uso', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: /Trabajar y estudiar/ })).toBeVisible(); await expect(page.locator('.tarjeta-rol')).toHaveCount(3); await expect(page.getByText(/sirve una vez y caduca/i)).toBeVisible(); }); test('un correo no registrado se rechaza con un mensaje claro', async ({ page }) => { await page.goto('/'); await page.fill('#email', 'nadie@ejemplo.com'); await page.click('#enviar'); await expect(page.locator('#error-entrada')).toContainText(/no está dado de alta/i); }); test('el acceso rápido entra al portal', async ({ page }) => { await page.goto('/'); await page.locator('.tarjeta-rol', { hasText: 'Empleado' }).click(); await page.waitForURL('**/portal'); await expect(page.locator('.usuario__txt b')).toHaveText(ROLES.empleado.nombre); }); test('un enlace ya usado no vuelve a servir', async ({ page }) => { const r = await page.request.post('/api/auth/enlace', { data: { email: ROLES.empleado.email } }); const { token } = await r.json(); await page.goto(`/acceso/${token}`); await expect(page.locator('.barra__nav .nav-item').first()).toBeVisible(); await page.request.post('/api/auth/salir'); await page.goto(`/acceso/${token}`); await expect(page.locator('#error-entrada')).toContainText(/ya se usó/i); }); test('un enlace inventado no entra', async ({ page }) => { await page.goto('/acceso/token-que-no-existe'); await expect(page.locator('#error-entrada')).toContainText(/no es válido/i); }); }); // --------------------------------------------------------------------- test.describe('Permisos', () => { for (const [clave, rol] of Object.entries(ROLES)) { test(`${clave} ve exactamente sus ${rol.secciones} secciones`, async ({ page }) => { await entrar(page, rol.email); await expect(page.locator('.barra__nav .nav-item[data-seccion]')).toHaveCount(rol.secciones); await expect(page.locator('.usuario__txt b')).toHaveText(rol.nombre); }); } test('el empleado no ve Equipo ni Bitácora', async ({ page }) => { await entrar(page, ROLES.empleado.email); for (const oculta of ['equipo', 'bitacora']) { await expect(page.locator(`.nav-item[data-seccion="${oculta}"]`)).toHaveCount(0); } }); test('el administrativo ve Equipo pero no la Bitácora', async ({ page }) => { await entrar(page, ROLES.administrativo.email); await expect(page.locator('.nav-item[data-seccion="equipo"]')).toHaveCount(1); await expect(page.locator('.nav-item[data-seccion="bitacora"]')).toHaveCount(0); }); test('la API niega lo que el menú oculta', async ({ page }) => { await entrar(page, ROLES.empleado.email); for (const ruta of ['/api/equipo', '/api/bitacora']) { const r = await page.request.get(ruta); expect(r.status(), `${ruta} debería negarse`).toBe(403); } }); test('sin sesión la API responde 401 y el portal manda a la entrada', async ({ page }) => { await page.goto('/'); expect((await page.request.get('/api/inicio')).status()).toBe(401); await page.goto('/portal'); await page.waitForURL('**/'); await expect(page.locator('.tarjeta-rol').first()).toBeVisible(); }); test('un intento denegado queda en la bitácora', async ({ page }) => { await entrar(page, ROLES.empleado.email); await page.request.get('/api/bitacora'); // 403 a propósito await page.request.post('/api/auth/salir'); await entrar(page, ROLES.director.email); const { entradas } = await (await page.request.get('/api/bitacora?limite=60')).json(); expect(entradas.some((e) => e.accion === 'acceso.denegado' && e.resultado === 'denegado')).toBeTruthy(); }); test('solo dirección ve los botones de editar el catálogo', async ({ page }) => { await entrar(page, ROLES.empleado.email); await irA(page, 'oferta'); await expect(page.locator('[data-editar-programa]')).toHaveCount(0); await irA(page, 'clases'); await expect(page.locator('[data-editar-clase]')).toHaveCount(0); await page.request.post('/api/auth/salir'); await entrar(page, ROLES.administrativo.email); await irA(page, 'oferta'); await expect(page.locator('[data-editar-programa]')).toHaveCount(0); await irA(page, 'clases'); await expect(page.locator('[data-editar-clase]').first()).toBeVisible(); await page.request.post('/api/auth/salir'); await entrar(page, ROLES.director.email); await irA(page, 'oferta'); await expect(page.locator('[data-editar-programa]').first()).toBeVisible(); }); }); // --------------------------------------------------------------------- test.describe('Oferta educativa', () => { test.beforeEach(async ({ page }) => { await entrar(page, ROLES.director.email); }); test('muestra los 8 programas en tarjetas', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tarjetas"]').click(); await expect(page.locator('.programa')).toHaveCount(8); await expect(page.getByRole('heading', { name: 'Licenciatura en Derecho' })).toBeVisible(); await expect(page.locator('.programa', { hasText: 'Psicología' }).locator('.precio b')).toHaveText('$1,500'); }); test('la vista de tabla trae las columnas del archivo de oferta', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tabla"]').click(); const tabla = page.locator('.tabla').first(); await expect(tabla.locator('tbody tr')).toHaveCount(8); // Debajo de 780 px el encabezado se oculta y cada celda lleva su propia // etiqueta en data-col, así que se comprueba la estructura, no la vista. for (const col of ['Programa', 'Nivel', 'Duración', 'Inicia', 'Inscripción', 'Mensualidad', 'Horarios', 'Validez oficial']) { await expect(tabla.locator('thead th', { hasText: col })).toHaveCount(1); await expect(tabla.locator(`tbody td[data-col="${col}"]`).first()).toBeVisible(); } await expect(tabla).toContainText('20211510'); // Derecho await expect(tabla).toContainText('$1,125'); // Bachillerato }); test('la elección entre tarjetas y tabla se recuerda al volver', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tabla"]').click(); await expect(page.locator('.tabla').first()).toBeVisible(); await irA(page, 'inicio'); await irA(page, 'oferta'); await expect(page.locator('[data-vista="tabla"]')).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('.programa')).toHaveCount(0); }); test('marca los programas sin RVOE confirmado y dice por qué', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tarjetas"]').click(); const bach = page.locator('.programa', { hasText: 'Bachillerato General' }); await expect(bach).toContainText(/Sin confirmar/i); await expect(bach).toContainText(/no lo cites/i); await expect(page.locator('.programa', { hasText: 'Trabajo Social' })).toContainText(/Por revisar/i); await expect(page.locator('.respuesta')).toContainText(/validez oficial sin confirmar/i); }); test('dirección edita un programa y el cambio se ve', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tarjetas"]').click(); await page.locator('.programa', { hasText: 'Licenciatura en Derecho' }).locator('[data-editar-programa]').click(); const dlg = page.locator('dialog.editor'); await expect(dlg).toBeVisible(); await expect(dlg.locator('input[name="nombre"]')).toHaveValue('Licenciatura en Derecho'); await dlg.locator('textarea[name="beneficios"]').fill('Beca del 25% y asesoría para titulación'); await dlg.locator('[data-guardar]').click(); await expect(dlg).toHaveCount(0); await expect(page.locator('.programa', { hasText: 'Licenciatura en Derecho' })).toContainText('asesoría para titulación'); // Se deja como estaba. await page.locator('.programa', { hasText: 'Licenciatura en Derecho' }).locator('[data-editar-programa]').click(); await page.locator('dialog.editor textarea[name="beneficios"]').fill('Beca del 25% en inscripción y mensualidad.'); await page.locator('dialog.editor [data-guardar]').click(); await expect(page.locator('dialog.editor')).toHaveCount(0); }); test('el editor rechaza un dato inválido y lo explica', async ({ page }) => { await irA(page, 'oferta'); await page.locator('[data-vista="tarjetas"]').click(); await page.locator('[data-editar-programa]').first().click(); const dlg = page.locator('dialog.editor'); await dlg.locator('input[name="nombre"]').fill(''); await dlg.locator('[data-guardar]').click(); await expect(dlg.locator('.editor__error')).toContainText(/Falta llenar/i); await expect(dlg).toBeVisible(); await dlg.locator('[data-cerrar]').first().click(); await expect(dlg).toHaveCount(0); }); test('dirección ve los datos pendientes de confirmar', async ({ page }) => { await irA(page, 'oferta'); const panel = page.locator('.placa', { hasText: 'Datos pendientes de confirmar' }); await expect(panel).toBeVisible(); await expect(panel).toContainText(/RVOE del Bachillerato/i); }); test('el empleado no ve el panel de datos pendientes', async ({ page }) => { await page.request.post('/api/auth/salir'); await entrar(page, ROLES.empleado.email); await irA(page, 'oferta'); await expect(page.locator('.placa', { hasText: 'Datos pendientes de confirmar' })).toHaveCount(0); }); }); // --------------------------------------------------------------------- test.describe('Links de clases', () => { test('se agrupan por programa y traen link para entrar y copiar', async ({ page }) => { await entrar(page, ROLES.empleado.email); await irA(page, 'clases'); await expect(page.locator('[data-bloque]')).toHaveCount(8); const psico = page.locator('[data-bloque]', { hasText: 'Licenciatura en Psicología' }); await expect(psico).toContainText('Psicopatología'); const fila = psico.locator('tbody tr', { hasText: 'Psicopatología' }); await expect(fila.locator('.clase-enlace')).toHaveAttribute('href', /^https:\/\/meet\.google\.com\//); await expect(fila.locator('.clase-enlace')).toHaveAttribute('target', '_blank'); await expect(fila).toContainText('Martes'); }); test('el filtro por programa deja solo ese bloque', async ({ page }) => { await entrar(page, ROLES.empleado.email); await irA(page, 'clases'); await page.locator('#filtros-clases .filtro', { hasText: 'Derecho' }).first().click(); const visibles = await page.locator('[data-bloque]:not([hidden])').count(); expect(visibles).toBe(1); await page.locator('#filtros-clases .filtro', { hasText: 'Todos los programas' }).click(); await expect(page.locator('[data-bloque]:not([hidden])')).toHaveCount(8); }); test('administración crea, edita y elimina una clase', async ({ page }, info) => { // Nombre único por ejecución: así el test no depende de que otro haya // limpiado, y un alta duplicada se vería de inmediato en el conteo. const materia = `Materia de prueba ${info.project.name} ${Date.now()}`; await entrar(page, ROLES.administrativo.email); await irA(page, 'clases'); await page.locator('.cabecera-seccion__acciones [data-agregar-clase]').click(); let dlg = page.locator('dialog.editor'); await expect(dlg).toBeVisible(); await dlg.locator('input[name="materia"]').fill(materia); await dlg.locator('input[name="enlace"]').fill('https://meet.google.com/prueba-auto-uno'); await dlg.locator('select[name="dias"]').selectOption('Viernes'); await dlg.locator('[data-guardar]').click(); await expect(dlg).toHaveCount(0); const fila = page.locator('tbody tr', { hasText: materia }); await expect(fila, 'guardar una vez debe crear exactamente una clase').toHaveCount(1); await fila.locator('[data-editar-clase]').click(); dlg = page.locator('dialog.editor'); await dlg.locator('input[name="docente"]').fill('Profa. Prueba'); await dlg.locator('[data-guardar]').click(); await expect(dlg).toHaveCount(0); await expect(page.locator('tbody tr', { hasText: materia })).toContainText('Profa. Prueba'); page.once('dialog', (d) => d.accept()); await page.locator('tbody tr', { hasText: materia }).locator('[data-editar-clase]').click(); await page.locator('dialog.editor [data-borrar]').click(); await expect(page.locator('dialog.editor')).toHaveCount(0); await expect(page.locator('tbody tr', { hasText: materia })).toHaveCount(0); // Y se confirma contra la API, no solo contra lo que se pintó. const { clases } = await (await page.request.get('/api/clases')).json(); expect(clases.filter((c) => c.materia === materia)).toHaveLength(0); }); test('rechaza un link que no es una dirección', async ({ page }) => { await entrar(page, ROLES.administrativo.email); await irA(page, 'clases'); await page.locator('.cabecera-seccion__acciones [data-agregar-clase]').click(); const dlg = page.locator('dialog.editor'); await dlg.locator('input[name="materia"]').fill('Prueba de link malo'); await dlg.locator('input[name="enlace"]').fill('meet.google.com/sin-esquema'); await dlg.locator('[data-guardar]').click(); await expect(dlg.locator('.editor__error')).toContainText(/https:\/\//); await expect(dlg.locator('input[name="enlace"]')).toHaveAttribute('aria-invalid', 'true'); await dlg.locator('[data-cerrar]').first().click(); }); test('el empleado ve los links pero no puede editarlos', async ({ page }) => { await entrar(page, ROLES.empleado.email); await irA(page, 'clases'); await expect(page.locator('.clase-enlace').first()).toBeVisible(); await expect(page.locator('[data-agregar-clase]')).toHaveCount(0); await expect(page.locator('[data-editar-clase]')).toHaveCount(0); }); }); // --------------------------------------------------------------------- test.describe('Contenido', () => { test.beforeEach(async ({ page }) => { await entrar(page, ROLES.director.email); }); test('los requisitos distinguen las 6 fotos del bachillerato', async ({ page }) => { await irA(page, 'requisitos'); await expect(page.locator('.placa', { hasText: 'Bachillerato' }).first()).toContainText('6 fotografías'); await expect(page.locator('.placa', { hasText: 'Licenciatura' }).first()).toContainText('4 fotografías'); await expect(page.getByText('Por experiencia profesional')).toBeVisible(); }); test('dirección agrega y quita un requisito', async ({ page }) => { await irA(page, 'requisitos'); await page.locator('.placa', { hasText: 'Maestría' }).locator('[data-agregar-requisito]').click(); const dlg = page.locator('dialog.editor'); await dlg.locator('input[name="texto"]').fill('Carta de motivos'); await dlg.locator('[data-guardar]').click(); await expect(dlg).toHaveCount(0); await expect(page.locator('.placa', { hasText: 'Maestría' })).toContainText('Carta de motivos'); page.once('dialog', (d) => d.accept()); await page.locator('.horario', { hasText: 'Carta de motivos' }).locator('[data-editar-requisito]').click(); await page.locator('dialog.editor [data-borrar]').click(); await expect(page.locator('dialog.editor')).toHaveCount(0); await expect(page.locator('.lienzo__interno')).not.toContainText('Carta de motivos'); }); test('becas explica que los referidos no se juntan y trae la escala', async ({ page }) => { await irA(page, 'becas'); await expect(page.locator('.respuesta')).toContainText(/no se junta/i); await expect(page.locator('.tabla', { hasText: 'Referidos' }).locator('tbody tr').last()).toContainText('$0'); await expect(page.locator('.placa', { hasText: 'Cuándo inicia cada programa' })).toContainText('Mayo 2026'); await expect(page.locator('.placa', { hasText: 'Cuándo inicia cada programa' })).toContainText('Enero 2026'); }); test('las preguntas frecuentes buscan de verdad', async ({ page }) => { await irA(page, 'preguntas'); const total = await page.locator('#resultados details').count(); expect(total).toBeGreaterThan(15); await page.fill('#buscar', 'referidos'); await expect.poll(async () => page.locator('#resultados details').count()).toBeLessThan(total); await expect(page.locator('#resultados')).toContainText('20%'); await page.fill('#buscar', 'zzzznoexiste'); await expect(page.locator('.vacio')).toBeVisible(); }); test('los mensajes se filtran por categoría', async ({ page }) => { await irA(page, 'mensajes'); const total = await page.locator('#lista > .placa').count(); expect(total).toBeGreaterThan(15); await page.locator('.filtro', { hasText: 'Objeciones' }).click(); const visibles = await page.locator('#lista > .placa:not([hidden])').count(); expect(visibles).toBeGreaterThan(0); expect(visibles).toBeLessThan(total); }); test('los datos del instituto traen la dirección correcta', async ({ page }) => { await irA(page, 'contacto'); await expect(page.locator('.respuesta')).toContainText('1409-d'); await expect(page.locator('.respuesta')).toContainText('58110'); await expect(page.locator('.lienzo__interno')).toContainText(/Trabajar y estudiar es posible/); await expect(page.locator('.lienzo__interno')).toContainText('443 356 0979'); }); test('dirección edita un dato del instituto', async ({ page }) => { await irA(page, 'contacto'); const editar = page.getByRole('button', { name: 'Editar Horario de atención' }); await editar.click(); const dlg = page.locator('dialog.editor'); await expect(dlg).toBeVisible(); const antes = await dlg.locator('textarea[name="valor"]').inputValue(); expect(antes).toContain('10:00'); await dlg.locator('textarea[name="valor"]').fill(`${antes} · verificado`); await dlg.locator('[data-guardar]').click(); await expect(dlg).toHaveCount(0); await expect(page.locator('.lienzo__interno')).toContainText('· verificado'); // Se deja como estaba. await page.getByRole('button', { name: 'Editar Horario de atención' }).click(); await page.locator('dialog.editor textarea[name="valor"]').fill(antes); await page.locator('dialog.editor [data-guardar]').click(); await expect(page.locator('dialog.editor')).toHaveCount(0); await expect(page.locator('.lienzo__interno')).not.toContainText('· verificado'); }); test('editar dos veces seguidas no abre dos ventanas', async ({ page }) => { await irA(page, 'contacto'); for (let i = 0; i < 3; i++) { await page.getByRole('button', { name: 'Editar WhatsApp' }).click(); await expect(page.locator('dialog.editor')).toHaveCount(1); await page.locator('dialog.editor [data-guardar]').click(); await expect(page.locator('dialog.editor')).toHaveCount(0); } }); test('el equipo muestra la matriz de quién ve qué', async ({ page }) => { await irA(page, 'equipo'); await expect(page.locator('.matriz')).toBeVisible(); const filaBitacora = page.locator('.matriz tbody tr', { hasText: 'Bitácora' }); await expect(filaBitacora.locator('td').nth(1)).toContainText('●'); // dirección await expect(filaBitacora.locator('td').nth(2)).toContainText('○'); // administración await expect(filaBitacora.locator('td').nth(3)).toContainText('○'); // empleado }); }); // --------------------------------------------------------------------- test.describe('Legibilidad y responsividad', () => { test('el texto base no baja de 17 px', async ({ page }) => { await entrar(page, ROLES.director.email); const tam = await page.evaluate(() => parseFloat(getComputedStyle(document.body).fontSize)); expect(tam).toBeGreaterThanOrEqual(17); }); test('el portal siempre usa tema claro, incluso si el sistema pide oscuro', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await entrar(page, ROLES.director.email); const fondo = await page.evaluate(() => getComputedStyle(document.body).backgroundColor); expect(fondo).toBe('rgb(250, 251, 252)'); const texto = await page.evaluate(() => getComputedStyle(document.body).color); expect(texto).toBe('rgb(20, 24, 29)'); }); test('los botones y campos llegan al alto mínimo para tocarse', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await entrar(page, ROLES.director.email); await irA(page, 'clases'); for (const sel of ['.nav-item', '.boton', '.clase-enlace']) { const caja = await page.locator(sel).first().boundingBox(); expect(caja.height, `${sel} es demasiado bajo para tocarse`).toBeGreaterThanOrEqual(38); } }); for (const ancho of [390, 768, 1024, 1440]) { test(`ninguna sección se desborda a ${ancho}px`, async ({ page }) => { await page.setViewportSize({ width: ancho, height: 900 }); await entrar(page, ROLES.director.email); for (const s of SECCIONES) { await irA(page, s); const desborde = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); expect(desborde, `${s} se desborda horizontalmente`).toBeLessThanOrEqual(1); } }); } test('la ventana de edición cabe en un teléfono', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await entrar(page, ROLES.director.email); await irA(page, 'clases'); await page.locator('.cabecera-seccion__acciones [data-agregar-clase]').click(); const dlg = page.locator('dialog.editor'); await expect(dlg).toBeVisible(); const caja = await dlg.boundingBox(); expect(caja.width).toBeLessThanOrEqual(390); expect(caja.x).toBeGreaterThanOrEqual(0); await dlg.locator('[data-cerrar]').first().click(); await expect(dlg).toHaveCount(0); }); test('en móvil el menú es un cajón que abre, navega y cierra', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await entrar(page, ROLES.director.email); await expect(page.locator('.tope')).toBeVisible(); const barra = page.locator('#barra'); await page.click('#abrir-menu'); await expect(barra).toHaveAttribute('data-abierta', '1'); await page.locator('.nav-item[data-seccion="oferta"]').click(); await expect(page.locator('.cabecera-seccion h1')).toHaveText('Oferta educativa'); await expect(barra).toHaveAttribute('data-abierta', '0'); }); test('la sección activa se anuncia con aria-current', async ({ page }) => { await entrar(page, ROLES.director.email); await irA(page, 'clases'); await expect(page.locator('.nav-item[data-seccion="clases"]')).toHaveAttribute('aria-current', 'page'); await expect(page.locator('.nav-item[aria-current="page"]')).toHaveCount(1); }); test('se puede navegar con el teclado desde la entrada', async ({ page }) => { await page.goto('/'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); const enfocado = await page.evaluate(() => document.activeElement?.className ?? ''); expect(enfocado).toContain('tarjeta-rol'); await page.keyboard.press('Enter'); await page.waitForURL('**/portal'); }); test('Escape cierra la ventana de edición sin guardar', async ({ page }) => { await entrar(page, ROLES.director.email); await irA(page, 'clases'); await page.locator('.cabecera-seccion__acciones [data-agregar-clase]').click(); await expect(page.locator('dialog.editor')).toBeVisible(); await page.keyboard.press('Escape'); await expect(page.locator('dialog.editor')).toHaveCount(0); await expect(page.locator('#barra')).not.toHaveAttribute('data-abierta', '1'); }); test('en teléfono los bloques largos llegan plegados y en escritorio abiertos', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await entrar(page, ROLES.director.email); await page.goto('/portal#/clases'); await expect(page.locator('.cabecera-seccion h1')).toBeVisible(); const plegables = page.locator('details.plegable'); await expect(plegables.first()).toBeVisible(); expect(await plegables.first().evaluate((d) => d.open)).toBe(false); // El resumen sí se ve y al pulsarlo se abre. await plegables.first().locator('summary').click(); expect(await plegables.first().evaluate((d) => d.open)).toBe(true); await page.setViewportSize({ width: 1440, height: 900 }); await page.goto('/portal#/clases'); await expect(page.locator('.cabecera-seccion h1')).toBeVisible(); expect(await page.locator('details.plegable').first().evaluate((d) => d.open)).toBe(true); await expect(page.locator('details.plegable summary').first()).toBeHidden(); }); test('ninguna sección se vuelve un scroll interminable en teléfono', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await entrar(page, ROLES.director.email); for (const s of SECCIONES) { await page.goto(`/portal#/${s}`); await expect(page.locator('.cabecera-seccion h1')).toBeVisible(); await expect(page.locator('.cargando')).toHaveCount(0); const alto = await page.evaluate(() => document.documentElement.scrollHeight); expect(alto, `${s} mide ${alto} px de alto en teléfono`).toBeLessThan(12000); } }); test('el logo del instituto se carga', async ({ page }) => { await entrar(page, ROLES.director.email); const ok = await page.locator('.barra__cabeza img').evaluate((img) => img.complete && img.naturalWidth > 0); expect(ok, 'el logo no se cargó').toBeTruthy(); }); });