mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
* feat(web): the Display tab is an ES-module page, with a page-visibility service (stage 4) - core/visibility.js: each page gets ctx.visibility (whileVisible, every, isVisible). Work registered there runs only while the page's tab is active and the browser tab visible, and ends when the page is swapped out. It reads the active tab from window.LEDVisibility, so it agrees with the classic partials. The registry gained a mountContext option for per-mount services. - pages/display.js replaces display.html's two inline scripts. The 5 s sync status poll runs through ctx.visibility.every; the status and scroll-speed hint requests go through ctx.api with ctx.signal, as does the Vegas order widget's plugin-list request. The Advanced toggle is a delegated data-action; window.updateSyncUI is a deprecated alias. - New DOM suites test_visibility_service.js and test_display_page.js; test_display_partial_ids.js imports the module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): no computed keys in the Display page's readout and destroy Codacy's object-injection rule flagged v[id] and ctx.state[name]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.6 KiB
JavaScript
117 lines
4.6 KiB
JavaScript
// The Display tab's page module must only look up elements the partial
|
|
// renders.
|
|
//
|
|
// Its brightness slider handler once also wrote to #brightness-display, a
|
|
// "LED brightness: N%" line that #387 removed from partials/display.html. The
|
|
// lookup returned null, so every movement of the slider threw a TypeError.
|
|
// This imports the shipped module (static/v3/js/pages/display.js), starts it
|
|
// on a fake root that answers only for the ids the partial's markup renders
|
|
// (null for any other, as in a browser), fires every listener it registered,
|
|
// and checks that every id it asked for exists. Then it moves the slider.
|
|
//
|
|
// No jsdom and no server needed.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { pathToFileURL } = require('url');
|
|
|
|
const PARTIAL = path.resolve(__dirname, '../../../web_interface/templates/v3/partials/display.html');
|
|
const JS = path.resolve(__dirname, '../../../web_interface/static/v3/js');
|
|
|
|
let pass = 0, fail = 0;
|
|
const ok = (label, cond, extra) => cond
|
|
? (pass++, console.log(' ok ' + label))
|
|
: (fail++, console.log(' FAIL ' + label + (extra !== undefined ? ' ' + JSON.stringify(extra) : '')));
|
|
|
|
const html = fs.readFileSync(PARTIAL, 'utf8');
|
|
const rendered = new Set([...html.matchAll(/\bid="([^"{}]+)"/g)].map(m => m[1]));
|
|
|
|
function fakeElement(id) {
|
|
const listeners = {};
|
|
const classes = new Set();
|
|
return {
|
|
id, value: '1', textContent: '', min: '', max: '', checked: false,
|
|
style: {}, dataset: {}, className: '',
|
|
classList: {
|
|
add: c => classes.add(c), remove: c => classes.delete(c),
|
|
contains: c => classes.has(c),
|
|
},
|
|
addEventListener: (type, fn) => { (listeners[type] ||= []).push(fn); },
|
|
dispatchEvent() { return true; },
|
|
appendChild() {},
|
|
getAttribute: () => null,
|
|
listeners,
|
|
};
|
|
}
|
|
|
|
(async () => {
|
|
console.log('\n── Display page module: element lookups ──');
|
|
const display = await import(pathToFileURL(path.join(JS, 'pages/display.js')).href);
|
|
|
|
const asked = new Set();
|
|
const elements = new Map();
|
|
const timers = [];
|
|
const win = {
|
|
setTimeout: fn => { timers.push(fn); return timers.length; },
|
|
clearTimeout() {},
|
|
URLSearchParams,
|
|
Event: class { constructor(type) { this.type = type; } },
|
|
PluginOrderList: { init() {} },
|
|
};
|
|
const doc = { defaultView: win, createElement: () => fakeElement(''), createTextNode: () => ({}) };
|
|
const rootListeners = {};
|
|
const root = {
|
|
ownerDocument: doc,
|
|
querySelector(sel) {
|
|
const m = /^#([\w-]+)$/.exec(sel);
|
|
if (!m) throw new Error('unexpected selector ' + sel);
|
|
asked.add(m[1]);
|
|
if (!rendered.has(m[1])) return null;
|
|
if (!elements.has(m[1])) elements.set(m[1], fakeElement(m[1]));
|
|
return elements.get(m[1]);
|
|
},
|
|
addEventListener: (type, fn) => { (rootListeners[type] ||= []).push(fn); },
|
|
contains: () => true,
|
|
};
|
|
const never = () => new Promise(() => {});
|
|
const polls = [];
|
|
const ctx = {
|
|
root, name: 'display', state: {}, signal: { aborted: false },
|
|
api: { get: never },
|
|
visibility: { every: (ms, fn) => { polls.push(ms); fn(); return () => {}; } },
|
|
};
|
|
|
|
let loadError = null;
|
|
try { display.init(root, ctx); } catch (e) { loadError = e; }
|
|
ok('init() runs', !loadError, loadError && String(loadError));
|
|
ok('the sync status is polled through ctx.visibility', polls.length === 1 && polls[0] === 5000, polls);
|
|
|
|
// Fire everything it wired, so every lookup it can make is made.
|
|
let thrown = null;
|
|
try {
|
|
for (const el of elements.values()) {
|
|
for (const fns of Object.values(el.listeners)) fns.forEach(fn => fn.call(el, { target: el }));
|
|
}
|
|
while (timers.length) timers.shift()();
|
|
} catch (e) { thrown = e; }
|
|
ok('its listeners and timers run without throwing', !thrown, thrown && String(thrown));
|
|
|
|
const missing = [...asked].filter(id => !rendered.has(id));
|
|
ok('it looks elements up', asked.size > 10, asked.size);
|
|
ok('every looked-up id is rendered by the partial', missing.length === 0, missing);
|
|
|
|
const slider = elements.get('brightness') || fakeElement('brightness');
|
|
const handlers = slider.listeners.input || [];
|
|
ok('the slider has an input handler', handlers.length > 0);
|
|
slider.value = '42';
|
|
thrown = null;
|
|
try { handlers.forEach(fn => fn.call(slider, { target: slider })); } catch (e) { thrown = e; }
|
|
ok('moving the slider throws nothing', !thrown, thrown && String(thrown));
|
|
const label = elements.get('brightness-value');
|
|
ok('...and shows the new value', !!label && label.textContent === '42', label && label.textContent);
|
|
|
|
display.destroy(root, ctx);
|
|
console.log(`\n${pass} passed, ${fail} failed\n`);
|
|
process.exit(fail ? 1 : 0);
|
|
})().catch(e => { console.error(e); process.exit(1); });
|