mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
feat(web): Schedule and General become ES-module pages (stage 3) (#767)
* feat(web): Schedule and General become ES-module pages (stage 3) Schedule and General follow stage 2 (#727): no inline scripts or inline handlers in either partial. Their code moves to static/v3/js/pages/schedule.js and pages/general.js, started per swap-in by the page registry. - Schedule: both pickers are drawn from the saved config carried as JSON in data-* attributes. The forms' hx-on save handlers become one htmx:afterRequest listener on the page; the forms are marked data-reports-result, which app.js now treats like an hx-on after-request handler, so a save still shows one notification. - General: the timezone picker reads data-timezone. The Security section's forms and buttons are delegated data-actions; requests go through core/api.js, so the login redirect is quiet, and a change made just before a swap is still reported. - handleScheduleResponse, handleDimScheduleResponse and webLogin stay as deprecated aliases through window.LEDMatrix. - New DOM suites test_schedule_page.js and test_general_page.js; the web login unit suite imports the module; test_es_modules.py pins the pages, the aliases, and the schedule config's round trip through its attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): no unused catch bindings or computed writes in the stage 3 modules Codacy flagged two unused catch variables and dynamic-key writes in pages/schedule.js and boot.js. The schedule config is read with getAttribute, and the default days and the webLogin alias object are built with Object.fromEntries. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,32 +3,24 @@
|
||||
// app.js marks a form data-dirty on any input in it and clears the mark only
|
||||
// after a successful htmx request; its beforeunload handler then asks "Leave
|
||||
// site?" while any visible form is still dirty. The token form posts with
|
||||
// fetch (window.webLogin.createToken in partials/general.html), so after a
|
||||
// token was created the form stayed dirty and reloading the page while the
|
||||
// General tab was open prompted about changes that had been saved.
|
||||
// fetch (createToken in static/v3/js/pages/general.js), so after a token was
|
||||
// created the form stayed dirty and reloading the page while the General tab
|
||||
// was open prompted about changes that had been saved.
|
||||
//
|
||||
// Runs the shipped inline script in a vm with a fake fetch and DOM -- no jsdom
|
||||
// and no server needed.
|
||||
// Imports the shipped page module and runs createToken with a fake fetch and
|
||||
// DOM -- no jsdom and no server needed.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
const PARTIAL = path.resolve(__dirname, '../../../web_interface/templates/v3/partials/general.html');
|
||||
const JS = path.resolve(__dirname, '../../../web_interface/static/v3/js');
|
||||
const load = f => import(pathToFileURL(path.join(JS, f)).href);
|
||||
|
||||
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) : '')));
|
||||
|
||||
function webLoginScript() {
|
||||
const html = fs.readFileSync(PARTIAL, 'utf8');
|
||||
const scripts = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script[^>]*>/gi)].map(m => m[1]);
|
||||
const found = scripts.find(s => s.includes('window.webLogin = {'));
|
||||
if (!found) throw new Error('webLogin script not found in general.html');
|
||||
return found;
|
||||
}
|
||||
|
||||
function el() {
|
||||
const classes = new Set(['hidden']);
|
||||
return {
|
||||
@@ -38,29 +30,28 @@ function el() {
|
||||
};
|
||||
}
|
||||
|
||||
function load(answer) {
|
||||
async function setup(answer) {
|
||||
const { createApi } = await load('core/api.js');
|
||||
const general = await load('pages/general.js');
|
||||
const elements = {
|
||||
'web-login-tokens': el(),
|
||||
'web-login-new-token-value': el(),
|
||||
'web-login-new-token': el(),
|
||||
'#web-login-tokens': el(),
|
||||
'#web-login-new-token-value': el(),
|
||||
'#web-login-new-token': el(),
|
||||
};
|
||||
const notes = [];
|
||||
const window = { showNotification: (m, t) => notes.push([m, t]), alert() {}, confirm: () => true };
|
||||
const context = {
|
||||
window, console,
|
||||
document: {
|
||||
getElementById: id => elements[id] || null,
|
||||
createElement: () => el(),
|
||||
querySelectorAll: () => [],
|
||||
},
|
||||
fetch: () => Promise.resolve({
|
||||
ok: answer.ok, status: answer.ok ? 200 : 400,
|
||||
json: () => Promise.resolve(answer.body),
|
||||
}),
|
||||
const doc = { createElement: () => el(), defaultView: { confirm: () => true } };
|
||||
const root = { ownerDocument: doc, querySelector: sel => elements[sel] || null, querySelectorAll: () => [] };
|
||||
const fetch = () => Promise.resolve({
|
||||
ok: answer.ok, status: answer.ok ? 200 : 400,
|
||||
headers: { get: () => null },
|
||||
text: () => Promise.resolve(JSON.stringify(answer.body)),
|
||||
});
|
||||
const ctx = {
|
||||
root, state: {}, signal: { aborted: false },
|
||||
api: createApi({ fetch }),
|
||||
notify: (m, t) => notes.push([m, t]),
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(webLoginScript(), context);
|
||||
return { webLogin: context.window.webLogin, elements, notes };
|
||||
return { general, ctx, elements, notes };
|
||||
}
|
||||
|
||||
function dirtyForm() {
|
||||
@@ -74,29 +65,25 @@ function dirtyForm() {
|
||||
};
|
||||
}
|
||||
|
||||
const flush = async () => { for (let i = 0; i < 10; i++) await new Promise(r => setImmediate(r)); };
|
||||
|
||||
(async () => {
|
||||
console.log('\n── General tab: API token form ──');
|
||||
|
||||
{
|
||||
const t = load({ ok: true, body: {
|
||||
const t = await setup({ ok: true, body: {
|
||||
status: 'success', message: 'Token created',
|
||||
data: { token: 'lmx_secret', record: { id: 't1', name: 'Home Assistant', prefix: 'lmx_sec' } },
|
||||
} });
|
||||
const form = dirtyForm();
|
||||
t.webLogin.createToken(form);
|
||||
await flush();
|
||||
ok('the new token is shown', t.elements['web-login-new-token-value'].textContent === 'lmx_secret');
|
||||
await t.general.createToken(t.ctx, form);
|
||||
ok('the new token is shown', t.elements['#web-login-new-token-value'].textContent === 'lmx_secret');
|
||||
ok('a created token leaves the form clean (no "Leave site?" on reload)',
|
||||
!form.hasAttribute('data-dirty'));
|
||||
}
|
||||
|
||||
{
|
||||
const t = load({ ok: false, body: { status: 'error', message: 'Name is required' } });
|
||||
const t = await setup({ ok: false, body: { status: 'error', message: 'Name is required' } });
|
||||
const form = dirtyForm();
|
||||
t.webLogin.createToken(form);
|
||||
await flush();
|
||||
await t.general.createToken(t.ctx, form);
|
||||
ok('a refused request reports the error', t.notes.some(([m, type]) => type === 'error' && /Name is required/.test(m)),
|
||||
t.notes);
|
||||
ok('...and keeps the form dirty: nothing was saved', form.hasAttribute('data-dirty'));
|
||||
|
||||
Reference in New Issue
Block a user