Files
insta-portal/tests/portal.spec.mjs
T
Uriel JarethandClaude Opus 5 20eb73d4aa Pruebas: apuntar a cualquier despliegue con BASE y no fijar el orden del tabulador
playwright.config.mjs toma BASE del entorno, y cuando no es local no arranca
ningun servidor. Asi la misma suite verifica el portal ya desplegado.

La prueba de teclado esperaba que el segundo Tab cayera en una tarjeta de
acceso; contra un servidor remoto las tarjetas todavia no estaban pintadas y
el foco caia en otra cosa. Ahora espera a que existan, tabula hasta
alcanzarlas y comprueba ademas que el foco se vea.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 00:02:17 -06:00

577 lines
28 KiB
JavaScript

// =====================================================================
// 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: '[email protected]', nombre: 'Marisol Aguilar', secciones: 10 },
administrativo: { email: '[email protected]', nombre: 'Rubén Cázares', secciones: 9 },
empleado: { email: '[email protected]', 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', '[email protected]');
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('/');
// Hay que esperar a que las tarjetas existan: se pintan cuando responde
// la API, y tabular antes deja el foco en cualquier otra cosa.
await expect(page.locator('.tarjeta-rol').first()).toBeVisible();
// Se tabula hasta llegar a una tarjeta de acceso, sin fijar cuántos
// saltos hacen falta: lo que se prueba es que sea alcanzable, no la
// posición exacta en el orden.
let enTarjeta = false;
for (let i = 0; i < 8 && !enTarjeta; i++) {
await page.keyboard.press('Tab');
enTarjeta = await page.evaluate(() => Boolean(document.activeElement?.closest?.('.tarjeta-rol')));
}
expect(enTarjeta, 'las tarjetas de acceso deben alcanzarse con el tabulador').toBeTruthy();
// Y el foco tiene que verse.
const contorno = await page.evaluate(() => getComputedStyle(document.activeElement).outlineStyle);
expect(contorno).not.toBe('none');
await page.keyboard.press('Enter');
await page.waitForURL('**/portal');
await expect(page.locator('.barra__nav .nav-item').first()).toBeVisible();
});
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();
});
});