Files
aia-website/www/tools/early-intervention-browser.mjs
T

68 lines
5.3 KiB
JavaScript

/** Guarded loopback-only campaign verification. Optional source build for visual comparison.
* PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs node tools/campaign-browser.mjs
* CAMPAIGN_REFERENCE_DIST=/tmp/reference/dist CAMPAIGN_BASELINE_DIST=/tmp/baseline ...
* Screenshots and evidence remain outside the repository. No submission tests.
*/
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import http from 'node:http';
import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url';
const root = fileURLToPath(new URL('../', import.meta.url));
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ? pathToFileURL(process.env.PLAYWRIGHT_MODULE).href : 'playwright');
const output = await fs.mkdtemp('/tmp/aia-early-browser-');
const evidence = { date: new Date().toISOString(), output, status: 'pending', error: '', browser: '', checks: [], blocked: [], screenshots: [], errors: [] };
const servers = [];
let browser;
const mime = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.webp': 'image/webp', '.png': 'image/png', '.json': 'application/json' };
async function serve(directory) {
const base=path.resolve(directory);
const server=http.createServer(async (req,res)=>{
try {
assert.ok(['GET','HEAD'].includes(req.method));
let file=path.resolve(base,'.'+decodeURIComponent(new URL(req.url,'http://localhost').pathname));
assert.ok(file.startsWith(base+'/') || file===base);
if((await fs.stat(file)).isDirectory()) file=path.join(file,'index.html');
res.setHeader('Content-Type',mime[path.extname(file)] || 'application/octet-stream');res.end(await fs.readFile(file));
} catch {res.statusCode=404;res.end('Not found');}
});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));servers.push(server);
return `http://127.0.0.1:${server.address().port}`;
}
try {
const origin=await serve(path.join(root,'dist'));
const reference=process.env.EARLY_REFERENCE_DIR ? await serve(process.env.EARLY_REFERENCE_DIR) : null;
const allowed=new Set([origin,reference].filter(Boolean));
browser=await chromium.launch({headless:true,executablePath:process.env.CHROMIUM_EXECUTABLE});
evidence.browser=browser.version();
for (const javaScriptEnabled of [true,false]) {
const ctx=await browser.newContext({javaScriptEnabled,serviceWorkers:'block',deviceScaleFactor:1});
await ctx.routeWebSocket('**/*',s=>s.close());
await ctx.route('**/*',r=>{const u=new URL(r.request().url());if(!allowed.has(u.origin)||!['GET','HEAD'].includes(r.request().method())){evidence.blocked.push({origin:u.origin,path:u.pathname,method:r.request().method()});return r.abort();}return r.continue();});
const page=await ctx.newPage();page.on('pageerror',e=>evidence.errors.push(e.message));
for(const width of javaScriptEnabled?[320,390,768,1440]:[390,1440]) {
await page.setViewportSize({width,height:900});
let source;
async function design(){return page.evaluate(()=>({headings:[...document.querySelectorAll('.qr-landing h1,.qr-landing h2,.qr-landing h3')].map(e=>e.textContent),heroWidth:document.querySelector('.qr-hero').getBoundingClientRect().width,font:getComputedStyle(document.querySelector('h1')).font,images:[...document.images].map(i=>({alt:i.alt,loaded:i.complete&&i.naturalWidth>0})),overflow:document.documentElement.scrollWidth>innerWidth}));}
async function visit(url){await page.goto(url,{waitUntil:'networkidle'});await page.evaluate(async()=>{await document.fonts.ready;document.querySelectorAll('img').forEach(i=>i.loading='eager');await Promise.all([...document.images].map(i=>i.decode().catch(()=>{})));});}
async function shot(label){const file=`${label}-${width}-${javaScriptEnabled?'js':'nojs'}.png`;const data=await page.screenshot({path:path.join(output,file),fullPage:true});evidence.screenshots.push({file,sha256:createHash('sha256').update(data).digest('hex')});}
if(reference&&javaScriptEnabled){await visit(reference+'/');source=await design();await shot('source');}
await visit(origin+'/lp/early-intervention');const actual=await design();
assert.equal(actual.overflow,false);assert.ok(actual.images.every(i=>i.loaded));
if(source){assert.deepEqual(actual.headings,source.headings);assert.equal(actual.heroWidth,source.heroWidth);assert.equal(actual.font,source.font);}
assert.equal(await page.locator('form').count(),0);
assert.equal(await page.locator('#consultation input:enabled').count(),0);
assert.equal(await page.locator('meta[name="robots"]').getAttribute('content'),'noindex,nofollow');
await page.locator('.qr-hero-actions a[href="#consultation"]').click();
assert.equal(new URL(page.url()).hash,'#consultation');
await page.evaluate(()=>scrollTo(0,0));await shot('import');
evidence.checks.push({width,javaScriptEnabled,sourceCompared:!!source,status:'passed',...actual});
}
await ctx.close();
}
assert.deepEqual(evidence.blocked,[]);assert.deepEqual(evidence.errors,[]);evidence.status='passed';
} catch(error){evidence.status='failed';evidence.error=String(error);process.exitCode=1;}
finally {if(browser)await browser.close();for(const s of servers)await new Promise(r=>s.close(r));await fs.writeFile(path.join(output,'evidence.json'),JSON.stringify(evidence,null,2));console.log(output,evidence.status,evidence.error);}