$('#prefs').onclick referenced a prefsModal that was never defined, and an
uncaught ReferenceError stops the whole script -- taking the theme toggle,
the feed filter, the event stream and loadFeeds() down with it, so the app
rendered an empty shell.
The scheduling patch had anchored on a function the rewrite already
deleted; str.replace matched nothing and said nothing.
tests/page-smoke.js executes the page against a stub DOM so this class of
failure is visible, since every server-side check passed while the UI was
completely dead. Handler wiring now skips a bad reference instead of
throwing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
67 lines
3.0 KiB
JavaScript
67 lines
3.0 KiB
JavaScript
// Executes web/index.html's script against a stub DOM and fails on anything thrown.
|
|
//
|
|
// This exists because a ReferenceError at load once blanked the whole UI: a patch
|
|
// anchored on a function that no longer existed, so `prefsModal` was referenced but
|
|
// never defined. `node --check` passes that happily -- it is a parse, not a run --
|
|
// and every server-side test passed too, because the server was fine.
|
|
//
|
|
// node tests/page-smoke.js
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'web', 'index.html'), 'utf8');
|
|
const script = html.split('<script>')[1].split('</script>')[0];
|
|
const ids = new Set([...html.matchAll(/id="([^"]+)"/g)].map(m => m[1]));
|
|
|
|
const missing = [];
|
|
const el = (name) => new Proxy({ style: {}, dataset: {}, classList: { add(){}, remove(){}, toggle(){}, contains(){ return false; } },
|
|
value: '', textContent: '', innerHTML: '', hidden: false, children: [], firstElementChild: null,
|
|
appendChild(){}, removeChild(){}, remove(){}, insertAdjacentHTML(){}, addEventListener(){},
|
|
setAttribute(){}, getAttribute(){ return null; }, select(){}, setSelectionRange(){}, focus(){},
|
|
replaceWith(){}, querySelector(){ return el('nested'); }, querySelectorAll(){ return []; },
|
|
play(){ return Promise.resolve(); }, pause(){}, closest(){ return null; } },
|
|
{ get: (t, k) => k in t ? t[k] : undefined, set: (t, k, v) => (t[k] = v, true) });
|
|
|
|
const document = {
|
|
querySelector(sel) {
|
|
if (sel.startsWith('#') && !ids.has(sel.slice(1))) { missing.push(sel); return null; }
|
|
return el(sel);
|
|
},
|
|
querySelectorAll: () => [],
|
|
createElement: () => el('created'),
|
|
addEventListener(){}, body: el('body'),
|
|
documentElement: { dataset: {} },
|
|
};
|
|
|
|
const ctx = {
|
|
document, console,
|
|
window: { isSecureContext: false, addEventListener(){} },
|
|
localStorage: { getItem: () => null, setItem(){}, removeItem(){} },
|
|
navigator: { clipboard: undefined, sendBeacon(){}, mediaSession: undefined },
|
|
fetch: () => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('') }),
|
|
EventSource: function () { this.close = () => {}; },
|
|
MediaMetadata: function () {},
|
|
Blob: function () {},
|
|
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
confirm: () => false, prompt: () => null, alert(){},
|
|
Date, Math, JSON, Object, Array, String, Number, Promise, Error, FormData: function(){},
|
|
URLSearchParams, encodeURIComponent, decodeURIComponent, parseInt, parseFloat, isNaN,
|
|
};
|
|
ctx.globalThis = ctx;
|
|
ctx.window.location = { href: '' };
|
|
|
|
try {
|
|
vm.createContext(ctx);
|
|
vm.runInContext(script, ctx, { filename: 'index.html<script>', timeout: 5000 });
|
|
} catch (e) {
|
|
console.error('FAIL: the page script threw while loading\n ' + e.stack.split('\n').slice(0, 3).join('\n '));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (missing.length) {
|
|
console.error('FAIL: handlers wired to elements that do not exist: ' + [...new Set(missing)].join(', '));
|
|
process.exit(1);
|
|
}
|
|
console.log('OK: page script loads clean, every selector it wires at load exists');
|