Files
ipx/tests/page-smoke.js
rays 4249393f2a An interface font and a reading font in Settings (#359)
The font chosen before is the interface's, under the same cookie, so a
choice already made stays as it was. The reading font, ipx_rfont, sets
an item's title and text and is the interface's until chosen. Text read
in a serif gets a line height of 1.75 for 1.65, whichever setting put
it there. Each face's stack is a custom property, used by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:35:02 +00:00

98 lines
4.8 KiB
JavaScript

// Builds a page from web/src as build.rs does, runs its script against a stub DOM, and fails
// on anything thrown: web/index.html, then web/admin.html in a second run of this file.
//
// 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 { makeContext, vm } = require('./dom-stub.js');
const PAGE = process.argv[2] || 'index.html';
const { buildPage } = require('../web/build.mjs');
// What ships: minified, so an id may have lost its quotes.
const { html, js: script, script: file } = buildPage(PAGE);
if (!/<link rel=stylesheet href="?\/app\.css\?v=[0-9a-f]{12}"?>/.test(html) && PAGE !== 'login.html') {
console.error(`FAIL: ${PAGE} does not load /app.css?v=<hash>`); process.exit(1);
}
if (!new RegExp(`<script src="?/${file.replace('.', '\\.')}\\?v=[0-9a-f]{12}"?>`).test(html)) {
console.error(`FAIL: the page does not load /${file}?v=<hash>`); process.exit(1);
}
const { ctx, missing } = makeContext(html, script);
try {
vm.createContext(ctx);
vm.runInContext(script, ctx, { filename: `${PAGE}<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);
}
// The modals are built on demand, so a load-time check never reaches them. Drive the
// ones that construct markup from live data, which is where a bad field reference hides.
const feed = {
id: 'f', url: 'https://x/rss', title: 'A Feed', image: null, folder: null,
keywords: ['a'], allow_explicit: false, auto_download: true, max_new_per_check: 3,
schedule: 'every 6h', schedule_mins: 360, every_mins: 360,
last_checked: 1, next_check: 2, entries: 1, downloaded: 0, unread: 1, last_error: null,
};
const drive = PAGE === 'admin.html' ? [
['drawServer', () => ctx.drawServer()],
['drawAccounts', () => ctx.drawAccounts()],
['drawLogView', () => ctx.drawLogView()],
] : [
['settingsModal', () => ctx.settingsModal(feed)],
['settingsModal (no override)', () => ctx.settingsModal({ ...feed, schedule: null, schedule_mins: null })],
['downloadLatestModal', () => ctx.downloadLatestModal(feed)],
['removeFeed', () => ctx.removeFeed(feed)],
['prefsModal', () => ctx.prefsModal()],
['opmlModal', () => ctx.opmlModal()],
['selectFeed (directory)', () => ctx.selectFeed(':directory')],
['selectFeed (currently listening)', () => ctx.selectFeed(':listening')],
['selectFeed (all subscriptions)', () => ctx.selectFeed(':all')],
['keysModal', () => ctx.keysModal()],
// `const S` is not reachable from here: top-level const/let do not become properties
// of a vm context the way var and function declarations do.
['groupHead', () => ctx.groupHead(feed, [{ ...feed, id: 'child', group: 'f', orphaned: true }])],
];
for (const [name, fn] of drive) {
try {
const r = fn();
if (r && typeof r.catch === 'function') r.catch(e => {
console.error(`FAIL: ${name} rejected: ${e.message}`); process.exit(1);
});
} catch (e) {
console.error(`FAIL: ${name} threw: ${e.message}`);
process.exit(1);
}
}
// A trace is one tap's (#219): the same id while requests keep coming, a new one after a quiet
// minute, so a refresh hours later is not filed under the last tap.
if (PAGE === 'index.html') {
const id = () => ctx.traceparent().split('-')[1];
vm.runInContext('Date.now = (n => () => n)(1e12)', ctx);
const first = id();
vm.runInContext('Date.now = (n => () => n)(1e12 + 30_000)', ctx);
if (id() !== first) { console.error('FAIL: a trace ended 30 s into a tap'); process.exit(1); }
vm.runInContext('Date.now = (n => () => n)(1e12 + 120_000)', ctx);
if (id() === first) { console.error('FAIL: a trace outlived a quiet minute'); process.exit(1); }
}
// theme.ts keeps the Settings theme controls in step when Settings is open, and looks before it
// touches them. The admin page has no Settings, so those are the ones it may ask for and not find.
const OPTIONAL = new Set(['#stheme', '#smode', '#smodefield', '#sfont', '#srfont']);
missing.splice(0, missing.length, ...missing.filter(sel => !OPTIONAL.has(sel)));
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}: its script loads clean, every selector it wires at load exists`);
// The admin page's log arms a poll timer; without an exit the pending interval keeps node alive.
if (PAGE === 'index.html') {
const r = require('child_process').spawnSync(process.execPath, [__filename, 'admin.html'], { stdio: 'inherit' });
process.exit(r.status);
}
process.exit(0);