File size: 4,242 Bytes
daea45b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 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);