Files
site-builder/craft/scripts/e2e-sitesmith-cleaned.mjs
T
shadowdaoandClaude Opus 5.5 465cf17bb0 Sitesmith: self-review render via review_render_upload; one-line-per-node summaries; real-browser check of cleaned output
- The render is uploaded with the new review_render_upload action (backend kind
  review_render, the only image a review job accepts); mocks follow.
- summarizeCanvas() never lets a newline or a quote into a node line (the server
  validates review summaries line by line); test.
- test:browser:sitesmith-cleaned (Firefox + Chromium): a page published from a
  cleaned ordinary Sitesmith response requests nothing outside the allow-listed
  hosts (own assets, fonts, Font Awesome CDN, images.pexels.com, placehold.co)
  and has no inline handlers or javascript:/data: URL attributes; the same
  response uncleaned is the control (requests example.com). All requests are
  answered or aborted locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:47:04 -08:00

83 lines
4.7 KiB
JavaScript

#!/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);