Base de conocimiento para el personal del Instituto Santiago Tapia: oferta educativa (en tarjetas y en tabla), requisitos y titulacion, becas y fechas de inscripcion, links de clases de Google Meet, preguntas frecuentes, mensajes listos y datos del instituto. Acceso con Magic Login: enlace de un solo uso, 15 minutos de vigencia, y en la base solo se guarda el SHA-256 del token. El canje es atomico, asi que dos canjes simultaneos del mismo enlace no pueden crear dos sesiones. Tres roles con 19 permisos. El catalogo lo edita direccion; los links de clases, direccion y administracion; el resto del equipo consulta. El menu se pinta con lo que el servidor autoriza y cada endpoint exige la misma clave, asi que no hay pantallas alcanzables escribiendo la direccion a mano. Toda edicion pasa por una lista blanca de campos validados y queda en la bitacora. Un precio vacio no se guarda como cero, un link que no es una direccion se rechaza, y no se puede marcar una validez oficial como confirmada sin numero de acuerdo. Pensado para leerse comodo: texto de 17 px, contraste AA, areas de toque de 44 px, tema claro y bloques plegados en telefono. La oferta, los costos, las becas, los requisitos y el contacto son los reales del instituto, tomados del CSV de oferta educativa y de la base de conocimiento. Los usuarios de acceso y los links de Meet son de demostracion. Verificado: 103 comprobaciones de API y 51 pruebas en WebKit escritorio y iPhone 13. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
151 lines
6.2 KiB
JavaScript
151 lines
6.2 KiB
JavaScript
// =====================================================================
|
|
// 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: '[email protected]',
|
|
administrativo: '[email protected]',
|
|
empleado: '[email protected]',
|
|
};
|
|
|
|
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 = `<!doctype html><meta charset="utf-8"><title>Capturas · Portal INSTA</title>
|
|
<style>
|
|
body{font:16px/1.6 system-ui;margin:0;padding:28px;background:#f3f7fc;color:#16283f}
|
|
h1{font-family:Palatino,Georgia,serif;margin:0 0 6px}
|
|
p{margin:0 0 24px;color:#45535f}
|
|
.g{display:grid;gap:18px;grid-template-columns:repeat(auto-fill,minmax(340px,1fr))}
|
|
figure{margin:0;background:#fff;border:1px solid #cfdce9;border-radius:20px 4px 20px 4px;overflow:hidden}
|
|
figcaption{padding:10px 14px;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#66737f;border-bottom:1px solid #e5edf5}
|
|
img{display:block;width:100%;height:auto}
|
|
</style>
|
|
<h1>Portal interno INSTA · capturas en WebKit</h1>
|
|
<p>${hechas.length} capturas · escritorio 1440, tableta 820, teléfono 390, las ventanas de edición y los tres menús por rol.</p>
|
|
<div class="g">
|
|
${hechas.map((h) => `<figure><figcaption>${h.nombre}</figcaption><img src="${h.archivo}" alt="${h.nombre}" loading="lazy"></figure>`).join('\n')}
|
|
</div>`;
|
|
|
|
await writeFile(path.join(SALIDA, 'index.html'), hoja, 'utf8');
|
|
console.log(`\n ${hechas.length} capturas en tests/capturas/ · abre tests/capturas/index.html\n`);
|