Frontend load test in CI, health page checks Seafile, OnlyOffice, audiobooks, podcasts and Mealie; roadmap updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-27 10:33:20 -04:00
1 parent 94d13b82d7
commit db58bfa982
7 files changed
+56 -13

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
// The browser side, without a browser: every script the page loads is run in order in one shared scope (as the browser does), against a stand-in for the page.
// This catches what only showed up in the interface before: a syntax error, a name declared twice by two files (which stops the second file and everything after it),
// a script that throws as it loads, a missing file, and a script listed in index.html twice or not at all.
const fs = require('fs'), vm = require('vm'), path = require('path'), assert = require('assert');
const PUB = path.join(__dirname, '..', 'public');
const html = fs.readFileSync(path.join(PUB, 'index.html'), 'utf8');
const listed = [...html.matchAll(/<script[^>]*\ssrc="(\/[\w.-]+\.js)"/g)].map(m => m[1]);
assert.ok(listed.length > 40, 'index.html lists the scripts');
assert.equal(new Set(listed).size, listed.length, 'no script is listed twice');
for (const f of listed) assert.ok(fs.existsSync(path.join(PUB, f)), f + ' is listed but missing');
const unlisted = fs.readdirSync(PUB).filter(f => f.endsWith('.js') && !listed.includes('/' + f) && !/^(sw|login|board|device-ui|webauthn)\.js$/.test(f)); // (the service worker and the other pages have their own)
assert.deepEqual(unlisted, [], 'every script in public/ is loaded by the page (or is one of the known others)');
// a stand-in for the browser that answers anything: any property is another stand-in, calling it gives one, awaiting it gives it back
const stub = () => { const f = function () {}; const p = new Proxy(f, { get: (t, k) => k === Symbol.toPrimitive ? () => '' : k === 'then' ? undefined : k === 'length' ? 0 : k === Symbol.iterator ? function* () {} : p, apply: () => p, construct: () => p, set: () => true, has: () => true }); return p; };
const store = {}, win = {};
const ctx = Object.assign(win, {
console: { log() {}, warn() {}, error() {}, info() {}, debug() {} }, document: stub(), navigator: Object.assign(stub(), { onLine: true, userAgent: 'test' }), location: { hash: '', href: 'http://x/', origin: 'http://x', pathname: '/', search: '', reload() {}, replace() {} }, history: stub(),
localStorage: { getItem: k => store[k] ?? null, setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }, sessionStorage: stub(), matchMedia: () => ({ matches: false, addEventListener() {}, addListener() {} }),
fetch: async () => ({ ok: true, status: 200, json: async () => ({}), text: async () => '', headers: { get: () => '' } }), addEventListener() {}, removeEventListener() {}, setTimeout: () => 0, clearTimeout() {}, setInterval: () => 0, clearInterval() {}, requestAnimationFrame: () => 0,
Audio: function () { return stub(); }, Image: function () { return stub(); }, MutationObserver: function () { return { observe() {}, disconnect() {} }; }, IntersectionObserver: function () { return { observe() {}, disconnect() {}, unobserve() {} }; }, ResizeObserver: function () { return { observe() {}, disconnect() {} }; },
URL, URLSearchParams, Blob, TextEncoder, TextDecoder, AbortController, Intl, Date, JSON, Math, Promise, Set, Map, WeakMap, Array, Object, String, Number, RegExp, Error, Symbol, Uint8Array, ArrayBuffer, DataView, encodeURIComponent, decodeURIComponent, parseInt, parseFloat, isNaN, structuredClone, atob, btoa,
screen: {}, innerWidth: 1000, innerHeight: 800, devicePixelRatio: 1, performance: { now: () => 0 }, crypto: stub(), indexedDB: undefined, caches: stub(), Notification: stub(), alert() {}, confirm: () => false, prompt: () => null, getComputedStyle: () => stub(), CSS: stub(),
});
ctx.window = ctx; ctx.self = ctx; ctx.globalThis = ctx; vm.createContext(ctx);
process.on('unhandledRejection', () => {}); // (boot.js starts the app against the stand-in; what it does after loading is not under test)
const failures = [];
for (const f of listed) {
const file = path.join(PUB, f), src = fs.readFileSync(file, 'utf8');
try { new vm.Script(src, { filename: f }); } catch (e) { failures.push(`${f}: syntax error: ${e.message}`); continue; }
try { vm.runInContext(src, ctx, { filename: f }); } catch (e) { failures.push(`${f}: ${e.name}: ${e.message}`); }
}
assert.deepEqual(failures, [], 'every script loads:\n ' + failures.join('\n '));
// the pieces the app calls by name across files are there
for (const name of ['route', 'viewHome', 'viewSettings', 'viewTasks', 'viewCalendar', 'viewBible', 'viewMail', 'viewRecipes', 'viewVideo', 'themeApply', 'autoSettings', 'bbToday', 'homeListening', 'saveToFiles', 'downloadText']) assert.equal(typeof vm.runInContext(`typeof ${name}`, ctx), 'string') && assert.equal(vm.runInContext(`typeof ${name}`, ctx), 'function', name + ' is defined');
console.log('frontend test ok (' + listed.length + ' scripts)'); process.exit(0);