Files
LEDMatrix/test/js/run_all.js
T
ChuckandClaude Opus 5.5 ec95340d4a feat(web): a start that waits for the display answers 202 and is delivered in the background
The start route held a request open for up to 45 s while a cold-started
display loaded its plugins; the MQTT bridge (15 s timeout) and browsers
reported a failure for a request that was then delivered.

Now, when no display is listening, the route starts the service if asked
and answers 202 with status "starting" at once. A single worker in the web
process (web_interface/on_demand_dispatch.py) sends the request until the
display acknowledges it or the wait runs out (45 s cold start, 10 s for a
running service without a socket yet). A newer start supersedes the
pending one; a stop cancels it (and succeeds, with cancelled_request_id,
even with no display listening). The outcome is reported by
/display/on-demand/status (source "web": starting, or error with
start-timeout or the socket's reason, until the display publishes
something newer) and by /display/current-status as on_demand_pending.

Callers: the web UI's on-demand modal and "Preview on display" treat
"starting" as taken (an info toast); the MQTT bridge already treats any
non-error 2xx as success (now pinned by a test).

Tests: the dispatcher (ack, retry then ack, start-timeout, other failures,
superseded, an in-flight ack for a superseded start, stop while pending,
a per-start wait, outcome lifetime); the routes (202, status routes while
pending and after a timeout, a later display state replacing the failure,
stop while pending, a new start superseding); a JS suite for app.js.
Mutation check: 20 mutants on the worker, the routes and app.js, 20 killed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:13:28 -04:00

86 lines
4.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Runs the web-interface JS suites.
//
// node run_all.js unit suites, plus DOM suites if a
// web interface is reachable
// BASE=http://<pi-ip>:5000 node run_all.js point the DOM suites at a rig
// REQUIRE_DOM=1 node run_all.js fail, rather than skip, when the DOM
// suites can't run (CI sets this)
//
// Unit suites need nothing but node. The DOM suites need `npm install` (jsdom)
// and a running web interface, because they deliberately test against the real
// server-rendered HTML and the real API rather than fixtures.
const { spawnSync } = require('child_process');
const http = require('http');
const path = require('path');
const fs = require('fs');
const BASE = process.env.BASE || 'http://localhost:5000';
const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js',
'unit/test_plugin_order_list.js',
'unit/test_html_escaping.js', 'unit/test_style_editor_element_keys.js',
'unit/test_style_editor_layout_leaf_columns.js',
'unit/test_style_editor_layout_leaf_collision.js',
'unit/test_update_all.js',
'unit/test_store_install.js',
'unit/test_install_polling.js',
'unit/test_store_categories.js',
'unit/test_github_url_install.js',
'unit/test_inline_handler_escaping.js',
'unit/test_plugin_action_delegation.js', 'unit/test_file_upload_widget.js',
'unit/test_store_registry_fields.js', 'unit/test_restart_banner.js',
'unit/test_page_registry.js', 'unit/test_core_modules.js',
'unit/test_overview_reconciliation_poll.js',
'unit/test_display_partial_ids.js',
'unit/test_general_web_login_token.js',
'unit/test_on_demand_starting.js'];
const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js',
'dom/test_tools_sections.js', 'dom/test_cache_page.js',
'dom/test_durations_page.js', 'dom/test_operation_history_page.js',
'dom/test_raw_json_page.js', 'dom/test_backup_restore_page.js',
'dom/test_schedule_page.js', 'dom/test_general_page.js',
'dom/test_visibility_service.js', 'dom/test_display_page.js'];
function reachable(url) {
return new Promise(res => {
const req = http.get(url, r => { r.resume(); res(r.statusCode < 500); });
req.on('error', () => res(false));
req.setTimeout(4000, () => { req.destroy(); res(false); });
});
}
function run(file) {
const r = spawnSync(process.execPath, [path.join(__dirname, file)],
{ stdio: 'inherit', cwd: __dirname, env: process.env });
return r.status === 0;
}
(async () => {
const results = [];
for (const f of UNIT) results.push([f, run(f)]);
const haveJsdom = fs.existsSync(path.join(__dirname, 'node_modules', 'jsdom'));
const up = await reachable(BASE + '/');
if (process.env.REQUIRE_DOM && (!haveJsdom || !up)) {
// A skipped suite reads as a pass in CI; say why and fail instead.
results.push([haveJsdom ? `DOM suites (no web interface at ${BASE})`
: 'DOM suites (jsdom not installed)', false]);
} else if (!haveJsdom) {
console.log(`\nSKIPPING DOM suites: jsdom not installed (run: npm install)\n`);
} else if (!up) {
console.log(`\nSKIPPING DOM suites: no web interface reachable at ${BASE}`);
console.log(` start one with: EMULATOR=true python3 web_interface/app.py`);
console.log(` or point at a rig: BASE=http://<host>:5000 node run_all.js\n`);
} else {
console.log(`\nDOM suites against ${BASE}\n`);
for (const f of DOM) results.push([f, run(f)]);
}
const failed = results.filter(([, ok]) => !ok);
console.log('\n─── summary ───');
results.forEach(([f, ok]) => console.log(` ${ok ? 'PASS' : 'FAIL'} ${f}`));
if (failed.length) { console.log(`\n${failed.length} suite(s) failed\n`); process.exit(1); }
console.log('\nall suites passed\n');
})();