#!/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. const ALLOWED_HOSTS = new Set(['site.test', 'fonts.googleapis.com', 'fonts.gstatic.com', 'cdnjs.cloudflare.com', 'images.pexels.com', 'placehold.co']); 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);