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>
98 lines
4.8 KiB
JavaScript
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);
|