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));