2026-11-01 11:47:04 -08:00
#!/usr/bin/env node
/**
* Real-browser check (Firefox AND Chromium, headless) for the Sitesmith apply-time cleaner (G8): a page
* published from a CLEANED model response loads nothing outside the allow-listed hosts and carries no
* inline event handlers or javascript: URLs. The same response UNCLEANED is loaded as a control, to
* show the check can see an off-list request. Nothing leaves the machine: every request is answered
* or aborted by page.route (off-site requests are only recorded).
* npm run test:browser:sitesmith-cleaned Env: CHROMIUM_PATH / FIREFOX_PATH
*/
import { chromium , firefox } from '@playwright/test' ;
import { build as esbuild } from 'esbuild' ;
import fs from 'node:fs' ;
import os from 'node:os' ;
import path from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
const root = path . resolve ( path . dirname ( fileURLToPath ( import . meta . url )), '..' );
const out = await esbuild ({
entryPoints : [ path . join ( root , 'scripts/e2e-sitesmith-cleaned-entry.ts' )],
bundle : true , write : false , format : 'esm' , platform : 'node' , jsx : 'automatic' , logLevel : 'error' ,
define : { 'process.env.NODE_ENV' : '"production"' , __EDITOR_BUILD__ : '"test"' },
banner : { js : "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" },
});
const tmp = path . join ( os . tmpdir (), `wsb-ss-cleaned- ${ process . pid } .mjs` );
fs . writeFileSync ( tmp , out . outputFiles [ 0 ]. text );
const ent = await import ( tmp );
fs . rmSync ( tmp , { force : true });
const SITE = 'http://site.test' ;
// The published page's own head resources (fonts, Font Awesome) + the cleaner's image hosts.
2026-11-02 10:15:04 -08:00
const ALLOWED_HOSTS = new Set ([ 'site.test' , 'fonts.googleapis.com' , 'fonts.gstatic.com' , 'cdnjs.cloudflare.com' , 'placehold.co' ]);
2026-11-01 11:47:04 -08:00
const PNG = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' , 'base64' );
let pass = 0 ; let fail = 0 ;
const ok = ( c , m , d = '' ) => { if ( c ) { pass ++ ; console . log ( 'PASS ' , m ); } else { fail ++ ; console . log ( 'FAIL ' , m , d ); } };
async function load ( browser , html ) {
const ctx = await browser . newContext ();
const page = await ctx . newPage ();
const hosts = new Set ();
await page . route ( '**/*' , async ( route ) => {
const u = new URL ( route . request (). url ());
hosts . add ( u . hostname );
if ( u . origin === SITE ) {
if ( u . pathname === '/' || u . pathname === '/index.html' ) return route . fulfill ({ status : 200 , contentType : 'text/html' , body : html });
if ( u . pathname . startsWith ( '/assets/' )) return route . fulfill ({ status : 200 , contentType : 'image/png' , body : PNG });
return route . fulfill ({ status : 404 , body : '' });
}
return route . abort (); // never contact a real host
});
await page . goto ( SITE + '/' , { waitUntil : 'load' });
await page . waitForTimeout ( 300 );
const dom = await page . evaluate (() => {
const handlers = [];
const jsUrls = [];
for ( const el of document . querySelectorAll ( '*' )) {
for ( const a of el . attributes ) {
if ( /^on/i . test ( a . name )) handlers . push ( ` ${ el . tagName } . ${ a . name } ` );
if ([ 'href' , 'src' , 'action' , 'formaction' , 'poster' , 'data' ]. includes ( a . name ) && /^\s*(javascript|vbscript|data):/i . test ( a . value )) jsUrls . push ( ` ${ el . tagName } . ${ a . name } ` );
}
}
return { handlers , jsUrls , text : document . body . innerText };
});
await ctx . close ();
return { hosts : [... hosts ]. sort (), dom };
}
for ( const [ name , launcher , exe ] of [[ 'chromium' , chromium , process . env . CHROMIUM_PATH ], [ 'firefox' , firefox , process . env . FIREFOX_PATH ]]) {
const browser = await launcher . launch ({ executablePath : exe || undefined , ...( name === 'chromium' ? { args : [ '--no-sandbox' ] } : {}) });
const control = await load ( browser , ent . render ( false ));
ok ( control . hosts . includes ( 'example.com' ), `[ ${ name } ] control (uncleaned): the check sees the off-list request to example.com` , JSON . stringify ( control . hosts ));
const cleaned = await load ( browser , ent . render ( true ));
const offList = cleaned . hosts . filter (( h ) => ! ALLOWED_HOSTS . has ( h ));
ok ( offList . length === 0 , `[ ${ name } ] cleaned: no request outside the allow-listed hosts` , JSON . stringify ( cleaned . hosts ));
ok ( cleaned . hosts . includes ( 'site.test' ), `[ ${ name } ] cleaned: the site's own asset was requested` , JSON . stringify ( cleaned . hosts ));
ok ( cleaned . dom . handlers . length === 0 , `[ ${ name } ] cleaned: no inline event handlers` , JSON . stringify ( cleaned . dom . handlers ));
ok ( cleaned . dom . jsUrls . length === 0 , `[ ${ name } ] cleaned: no javascript:/data: URLs in URL attributes` , JSON . stringify ( cleaned . dom . jsUrls ));
ok ( /Fresh bread every morning/ . test ( cleaned . dom . text ) && /Book a table/ . test ( cleaned . dom . text ), `[ ${ name } ] cleaned: the ordinary content is all there` );
await browser . close ();
}
console . log ( fail ? `\nFAILED ( ${ fail } )` : `\nALL PASS ( ${ pass } )` );
process . exit ( fail ? 1 : 0 );