smolcode / engine /webcheck.js
seanpoyner's picture
Upload folder using huggingface_hub
daea45b verified
Raw
History Blame Contribute Delete
4.24 kB
// Headless smoke-check for a model-built web app, used by smolbuilder so the
// agent can actually *test* what it builds (the web equivalent of run_python).
//
// Loads index.html in jsdom, runs its scripts, then clicks every <button>, and
// reports any JavaScript errors. The goal is high precision: a correct app
// reports zero errors; a broken one (null element refs, undefined functions,
// syntax errors, exceptions on click) reports them so the agent can fix it.
//
// We stub the browser APIs jsdom doesn't implement (canvas 2d/webgl context,
// alert/confirm/prompt, matchMedia, media play) so apps that *use* them aren't
// falsely flagged — we're checking the app's own logic, not jsdom's coverage.
//
// Output: a single JSON line {ok, errors, buttons, clicked}. Exit 0 always
// (the verdict is in the JSON); exit 3 only if jsdom itself is missing.
'use strict';
let JSDOM, VirtualConsole;
try {
({ JSDOM, VirtualConsole } = require('jsdom'));
} catch (e) {
process.stdout.write(JSON.stringify({ ok: null, infra: 'jsdom not installed' }) + '\n');
process.exit(3);
}
const fs = require('fs');
function makeCtx() {
// A permissive 2d/webgl context stub: method calls no-op, the few methods
// whose *return value* is used hand back something safe to deref.
return new Proxy({}, {
get(_t, p) {
if (p === 'measureText') return () => ({ width: 0 });
if (p === 'getImageData') return () => ({ data: new Uint8ClampedArray(4), width: 1, height: 1 });
if (p === 'createLinearGradient' || p === 'createRadialGradient' || p === 'createPattern')
return () => ({ addColorStop() {} });
if (p === 'canvas') return { width: 300, height: 150 };
return () => undefined;
},
set() { return true; },
});
}
function stubBrowser(window) {
try { window.HTMLCanvasElement.prototype.getContext = () => makeCtx(); } catch (e) {}
const noop = () => {};
window.alert = noop;
window.confirm = () => true;
window.prompt = () => '';
window.scrollTo = noop;
window.scroll = noop;
if (!window.matchMedia)
window.matchMedia = () => ({ matches: false, media: '', addListener: noop, removeListener: noop, addEventListener: noop, removeEventListener: noop });
try { window.HTMLMediaElement.prototype.play = () => Promise.resolve(); } catch (e) {}
try { window.HTMLMediaElement.prototype.pause = noop; } catch (e) {}
}
const file = process.argv[2];
const html = fs.readFileSync(file, 'utf8');
const errors = [];
const push = (m) => { if (m && errors.indexOf(m) === -1) errors.push(String(m).slice(0, 400)); };
const vc = new VirtualConsole();
vc.on('jsdomError', (e) => push('script error: ' + (e && e.detail ? (e.detail.message || e.detail) : (e && e.message))));
let dom;
try {
dom = new JSDOM(html, {
runScripts: 'dangerously',
pretendToBeVisual: true,
virtualConsole: vc,
beforeParse(window) {
stubBrowser(window);
window.addEventListener('error', (ev) => push('uncaught: ' + (ev.error ? (ev.error.message || ev.error) : ev.message)));
window.addEventListener('unhandledrejection', (ev) => push('promise rejection: ' + (ev.reason && ev.reason.message ? ev.reason.message : ev.reason)));
},
});
} catch (e) {
push('load failed: ' + e.message);
process.stdout.write(JSON.stringify({ ok: false, errors, buttons: 0, clicked: 0 }) + '\n');
process.exit(0);
}
const { window } = dom;
const doc = window.document;
function clickAll() {
const buttons = Array.from(doc.querySelectorAll('button, [onclick], input[type=button], input[type=submit]'));
let clicked = 0;
for (const el of buttons) {
try {
if (el.disabled) el.disabled = false; // exercise the handler regardless of initial state
el.click();
clicked++;
} catch (e) {
push('click "' + (el.textContent || el.id || el.tagName).trim().slice(0, 30) + '": ' + e.message);
}
}
return { n: buttons.length, clicked };
}
// Let inline scripts settle, click, then let one timer tick surface late errors.
setTimeout(() => {
const { n, clicked } = clickAll();
setTimeout(() => {
process.stdout.write(JSON.stringify({ ok: errors.length === 0, errors, buttons: n, clicked }) + '\n');
process.exit(0);
}, 250);
}, 50);