// ===================================================================== // Capturas de referencia en WebKit. // node tests/capturas.mjs // Deja los PNG en tests/capturas/ y una hoja de contactos HTML para // revisar el portal completo de un vistazo. // ===================================================================== import { webkit } from '@playwright/test'; import { mkdir, writeFile, rm } from 'node:fs/promises'; import path from 'node:path'; const BASE = process.env.BASE ?? 'http://localhost:4180'; const SALIDA = path.resolve('tests/capturas'); const SECCIONES = ['inicio', 'oferta', 'requisitos', 'becas', 'clases', 'preguntas', 'mensajes', 'contacto', 'equipo', 'bitacora']; const VISTAS = [ { id: 'escritorio', ancho: 1440, alto: 900 }, { id: 'tableta', ancho: 820, alto: 1100 }, { id: 'movil', ancho: 390, alto: 844 }, ]; const CUENTAS = { director: 'director@insta.demo', administrativo: 'administracion@insta.demo', empleado: 'asesor@insta.demo', }; await rm(SALIDA, { recursive: true, force: true }); await mkdir(SALIDA, { recursive: true }); const navegador = await webkit.launch(); const hechas = []; async function capturar(ctx, nombre, url) { const page = await ctx.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); await page.waitForTimeout(450); // deja terminar la entrada en escena const archivo = `${nombre}.png`; const destino = path.join(SALIDA, archivo); const alto = await page.evaluate(() => document.documentElement.scrollHeight); // WebKit no puede capturar más de 32767 px de alto. Si una sección se pasa, // es en sí mismo un aviso: se corta la captura y se anota. if (alto > 30000) { console.log(` ⚠ ${nombre} mide ${alto} px de alto: se captura solo la primera pantalla`); await page.screenshot({ path: destino }); hechas.push({ archivo, nombre: `${nombre} (recortada · ${alto} px)` }); } else { await page.screenshot({ path: destino, fullPage: true }); hechas.push({ archivo, nombre: `${nombre} · ${alto} px` }); } await page.close(); } async function abrirSesion(ctx, email) { const r = await ctx.request.post(`${BASE}/api/auth/enlace`, { data: { email } }); const { token } = await r.json(); const p = await ctx.newPage(); await p.goto(`${BASE}/acceso/${token}`); await p.waitForSelector('.barra__nav .nav-item'); await p.close(); } for (const v of VISTAS) { const ctx = await navegador.newContext({ viewport: { width: v.ancho, height: v.alto }, colorScheme: 'light', locale: 'es-MX', deviceScaleFactor: 1, }); await capturar(ctx, `${v.id}-00-entrada`, `${BASE}/`); await abrirSesion(ctx, CUENTAS.director); let i = 1; for (const s of SECCIONES) { await capturar(ctx, `${v.id}-${String(i++).padStart(2, '0')}-${s}`, `${BASE}/portal#/${s}`); } await ctx.close(); console.log(` ✔ ${v.id} (${v.ancho} px)`); } // La oferta en vista de tabla, que es la alterna { const ctx = await navegador.newContext({ viewport: { width: 1440, height: 900 }, locale: 'es-MX' }); await abrirSesion(ctx, CUENTAS.director); const p = await ctx.newPage(); await p.goto(`${BASE}/portal#/oferta`, { waitUntil: 'networkidle' }); await p.locator('[data-vista="tabla"]').click(); await p.waitForTimeout(400); await p.screenshot({ path: path.join(SALIDA, 'escritorio-oferta-tabla.png'), fullPage: true }); hechas.push({ archivo: 'escritorio-oferta-tabla.png', nombre: 'oferta en tabla' }); // La ventana de edición de un programa await p.locator('[data-vista="tarjetas"]').click(); await p.locator('[data-editar-programa]').first().click(); await p.waitForTimeout(350); await p.screenshot({ path: path.join(SALIDA, 'escritorio-editor-programa.png') }); hechas.push({ archivo: 'escritorio-editor-programa.png', nombre: 'editor de programa' }); await p.close(); await ctx.close(); } // La ventana de edición de una clase, en teléfono { const ctx = await navegador.newContext({ viewport: { width: 390, height: 844 }, locale: 'es-MX' }); await abrirSesion(ctx, CUENTAS.administrativo); const p = await ctx.newPage(); await p.goto(`${BASE}/portal#/clases`, { waitUntil: 'networkidle' }); await p.locator('.cabecera-seccion__acciones [data-agregar-clase]').click(); await p.waitForTimeout(350); await p.screenshot({ path: path.join(SALIDA, 'movil-editor-clase.png') }); hechas.push({ archivo: 'movil-editor-clase.png', nombre: 'editor de clase en móvil' }); await p.close(); await ctx.close(); } // El menú de cada rol, para ver la diferencia de permisos de un vistazo for (const [rol, email] of Object.entries(CUENTAS)) { const ctx = await navegador.newContext({ viewport: { width: 1180, height: 1000 }, locale: 'es-MX' }); await abrirSesion(ctx, email); const p = await ctx.newPage(); await p.goto(`${BASE}/portal#/inicio`, { waitUntil: 'networkidle' }); await p.waitForTimeout(450); await p.screenshot({ path: path.join(SALIDA, `rol-${rol}.png`) }); hechas.push({ archivo: `rol-${rol}.png`, nombre: `menú de ${rol}` }); await p.close(); await ctx.close(); } console.log(' ✔ menús por rol y ventanas de edición'); await navegador.close(); const hoja = `Capturas · Portal INSTA

Portal interno INSTA · capturas en WebKit

${hechas.length} capturas · escritorio 1440, tableta 820, teléfono 390, las ventanas de edición y los tres menús por rol.

${hechas.map((h) => `
${h.nombre}
${h.nombre}
`).join('\n')}
`; await writeFile(path.join(SALIDA, 'index.html'), hoja, 'utf8'); console.log(`\n ${hechas.length} capturas en tests/capturas/ · abre tests/capturas/index.html\n`);