Files

141 lines
6.5 KiB
JavaScript

import pw from '/home/claude/.npm-global/lib/node_modules/playwright/index.js';
import http from 'http'; import fs from 'fs'; import path from 'path';
import { execSync } from 'child_process';
const { chromium } = pw;
/* ioon-verificar · los seis escenarios adversos + lo que se mide con número.
Se sirve dist/ en 127.0.0.1 y se mide ahí, no en el disco. */
const RAIZ = 'dist';
const TIPO = { '.html':'text/html;charset=utf-8', '.css':'text/css', '.js':'text/javascript',
'.woff2':'font/woff2', '.png':'image/png', '.json':'application/json' };
const srv = http.createServer((req,res)=>{
let p = decodeURIComponent(req.url.split('?')[0]);
if (p.endsWith('/')) p += 'index.html';
const f = path.join(RAIZ, p);
if (!fs.existsSync(f) || fs.statSync(f).isDirectory()) { res.writeHead(404); return res.end('404'); }
res.writeHead(200, { 'content-type': TIPO[path.extname(f)] ?? 'application/octet-stream' });
res.end(fs.readFileSync(f));
});
await new Promise(r => srv.listen(4321, '127.0.0.1', r));
const BASE = 'http://127.0.0.1:4321';
const RUTAS = ['/', '/ficha/hombres-necios/', '/ficha/nada-te-turbe/'];
const b = await chromium.launch({ executablePath:'/opt/pw-browsers/chromium' });
const R = {};
/* ── 1 · sin JavaScript ─────────────────────────────────────────────────── */
{
const c = await b.newContext({ javaScriptEnabled:false, viewport:{width:1280,height:900} });
const p = await c.newPage();
R.sinJS = [];
for (const r of RUTAS) {
await p.goto(BASE+r, { waitUntil:'load' });
R.sinJS.push({ ruta:r,
textoVisible: await p.evaluate(()=>document.body.innerText.trim().length),
h1: await p.evaluate(()=>document.querySelector('h1')?.innerText ?? null),
enlaces: await p.evaluate(()=>document.querySelectorAll('a').length) });
}
await c.close();
}
/* ── 2 · movimiento reducido ────────────────────────────────────────────── */
{
const c = await b.newContext({ reducedMotion:'reduce', viewport:{width:1280,height:900} });
const p = await c.newPage(); await p.goto(BASE+RUTAS[1]);
R.movReducido = await p.evaluate(()=>{
const a = document.querySelector('main a') ?? document.querySelector('a');
return { duracion: getComputedStyle(a).transitionDuration,
textoVisible: document.body.innerText.trim().length };
});
await c.close();
}
/* ── 3 · pestaña oculta ─────────────────────────────────────────────────── */
{
const c = await b.newContext({ viewport:{width:1280,height:900} });
const p = await c.newPage();
await p.addInitScript(() => {
Object.defineProperty(document,'visibilityState',{get:()=>'hidden'});
Object.defineProperty(document,'hidden',{get:()=>true});
window.requestAnimationFrame = () => 0; // rAF congelado
});
await p.goto(BASE+RUTAS[1]);
R.oculta = await p.evaluate(()=>{
const inv = [...document.querySelectorAll('h1,h2,p,li')]
.filter(e=>e.innerText.trim() && getComputedStyle(e).opacity !== '1').length;
return { textoVisible: document.body.innerText.trim().length, elementosInvisibles: inv };
});
await c.close();
}
/* ── 4 · 360 px · 5 · táctil · 6 · objetivos de toque ───────────────────── */
{
for (const [nom, opc] of [
['ancho360', { viewport:{width:360,height:740} }],
['tactil', { viewport:{width:390,height:844}, hasTouch:true, isMobile:true,
userAgent:'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' }]]) {
const c = await b.newContext(opc); const p = await c.newPage();
R[nom] = [];
for (const r of RUTAS) {
await p.goto(BASE+r);
R[nom].push(await p.evaluate(ruta => {
const d = document.documentElement;
const toques = [...document.querySelectorAll('a')].map(a=>{
const b = a.getBoundingClientRect();
return { texto:a.innerText.trim().slice(0,22), alto:+b.height.toFixed(1), ancho:+b.width.toFixed(1) };
}).filter(t=>t.alto>0);
return { ruta,
desbordeH: d.scrollWidth - d.clientWidth,
peorToque: Math.min(...toques.map(t=>t.alto)),
bajo24: toques.filter(t=>t.alto < 24).map(t=>t.texto),
bajo44: toques.filter(t=>t.alto < 44).map(t=>t.texto) };
}, r));
}
await c.close();
}
}
/* ── lo que se mide con número ──────────────────────────────────────────── */
{
const c = await b.newContext({ viewport:{width:1280,height:900} });
const p = await c.newPage();
const red = [];
p.on('response', r => red.push({ url:r.url(), tipo:r.request().resourceType() }));
await p.goto(BASE+RUTAS[1], { waitUntil:'networkidle' });
R.recursos = red.map(r=>({ tipo:r.tipo, url:r.url.replace(BASE,'') }));
R.js = red.filter(r=>r.tipo==='script').length;
/* anchos durante el hover: un enlace que ensancha empuja a sus vecinos */
R.hover = await p.evaluate(async () => {
const a = document.querySelector('main a') ?? document.querySelector('header a');
if (!a) return null;
const antes = a.getBoundingClientRect().width;
a.dispatchEvent(new MouseEvent('mouseover',{bubbles:true}));
a.classList.add('__h');
await new Promise(r=>setTimeout(r,200));
return { antes:+antes.toFixed(2), despues:+a.getBoundingClientRect().width.toFixed(2) };
});
R.meta = await p.evaluate(()=>({
lang: document.documentElement.lang,
titulo: document.title,
descripcion: document.querySelector('meta[name=description]')?.content?.length ?? 0,
ogImage: document.querySelector('meta[property="og:image"]')?.content ?? null,
canonica: document.querySelector('link[rel=canonical]')?.href ?? null,
jsonld: !!document.querySelector('script[type="application/ld+json"]'),
saltar: !!document.querySelector('.saltar'),
h1: document.querySelectorAll('h1').length,
}));
await c.close();
}
await b.close(); srv.close();
/* bytes en el cable, comprimidos */
const gz = (f) => +(execSync(`gzip -c9 ${f} | wc -c`).toString().trim());
R.bytes = {
indexHTML: gz('dist/index.html'),
fichaHTML: gz('dist/ficha/hombres-necios/index.html'),
fuenteWOFF2: fs.statSync('dist/Commissioner-wght.woff2').size,
ogPNG: fs.statSync('dist/og/hombres-necios.png').size,
};
console.log(JSON.stringify(R, null, 1));