mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 08:50:23 +00:00
Takes over #1985 by @SaarMesh-Bot, as offered there on 2026-09-16 and 2026-09-17. **Their commit is the first here, unchanged and under their authorship**; the rest clears the two review points. Closing #1985 in favour of this so the rebase and the fixes travel together, not to reassign the work. ## The feature, unchanged The frontend never surfaced which build was running, though `/api/health` has reported `{version, commit, buildTime}` all along. A footer on the nav drawer now renders `CoreScope <version>`, linking to the releases page, with commit and build time in the tooltip. Colours come from existing CSS variables, the label is set with `textContent`, and a failed health call leaves a neutral label rather than an empty footer — all as the author wrote it. ## Review point 1: fetch on open, not on page load The `/api/health` call sat in `buildDom()`, which runs on page load. The drawer may never be opened, and **cannot** be opened at ≤768px, where the module is disabled by design. So every visitor's browser was requesting the endpoint to fill a footer most of them would never see. Moved into `open()`, after the width gate. `fetchVersion` still caches its promise for the page lifetime, so re-opening costs nothing. ## Review point 2: the fallback is pinned `tests/unit/test-nav-drawer-version-footer.js`, six cases: - a rejected fetch, a non-ok response, and a 200 without `version` each leave the neutral `CoreScope` label — not a blank footer and not `CoreScope undefined`, which is what an instance shows exactly when someone is trying to read its version - a normal response renders the version, with commit and build time in the tooltip - a version carrying markup lands verbatim in `textContent`, and `innerHTML` is never touched - the endpoint is requested once however often the footer is filled, pinning the cache from the other side It **slices `fetchVersion` and `fillVersion` out of the shipped `public/nav-drawer.js`** and evaluates them rather than copying them into the test, so it exercises what ships — same approach as `tests/unit/test-direct-rf-heard-by.js`. Both slice markers are asserted, so a rename fails loudly instead of quietly testing nothing. Each case re-evaluates the slice, because `versionPromise` caches for the page lifetime and a shared sandbox would hand the second case the first case's answer. Listed in `test-all.sh`, which per #2036 is the only frontend runner. ## A note on the third commit The first version of the test used `setImmediate` to settle the promise queue. It ran fine under node and **failed eslint**, which treats these as browser code. I ran eslint and committed in the same command and pushed without reading its output. Fixed in the commit after, using `setTimeout(r, 0)`. Recording it because the PR would otherwise show a lint failure in its history with no explanation. ## Verification 6 of 6 in the new suite, eslint clean on both changed files, `test-test-inventory.js` passes with the new file classified. Cherry-picked cleanly onto current master. --------- Co-authored-by: SaarMesh-Bot <bot@saarmesh.de> Co-authored-by: Claude <noreply@anthropic.com>
121 lines
5.2 KiB
JavaScript
121 lines
5.2 KiB
JavaScript
/* test-nav-drawer-version-footer.js
|
|
*
|
|
* The nav drawer's footer reports the running build from GET /api/health
|
|
* (#1985). Two behaviours are easy to lose in a later edit and are pinned here:
|
|
*
|
|
* 1. A failed or version-less /api/health leaves the neutral "CoreScope"
|
|
* label. Without this the footer can end up blank or reading
|
|
* "CoreScope undefined" on any instance whose health endpoint is
|
|
* unavailable, which is exactly when someone is looking at it.
|
|
* 2. The version is written with textContent, never as markup. The string
|
|
* comes from the server, and a footer is a poor place to learn that.
|
|
*
|
|
* The functions are sliced out of the shipped public/nav-drawer.js and
|
|
* evaluated, rather than copied here, so this tests what actually ships. Same
|
|
* approach as tests/unit/test-direct-rf-heard-by.js. The rest of the module is
|
|
* an IIFE that builds real DOM and binds pointer handlers, which is E2E
|
|
* territory; these two are pure enough to run without a browser.
|
|
*/
|
|
'use strict';
|
|
|
|
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const assert = require('assert');
|
|
|
|
let passed = 0, failed = 0;
|
|
async function test(name, fn) {
|
|
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
|
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
const SRC = fs.readFileSync(path.join(REPO_ROOT, 'public/nav-drawer.js'), 'utf8');
|
|
|
|
// Slice from the cached-promise declaration to the constants that follow
|
|
// fillVersion. If either marker moves the slice fails loudly rather than
|
|
// silently testing nothing.
|
|
function sliceVersionFns() {
|
|
const start = SRC.indexOf('var versionPromise = null;');
|
|
const end = SRC.indexOf('var EDGE_PX');
|
|
assert.ok(start !== -1, 'could not find "var versionPromise = null;" in public/nav-drawer.js');
|
|
assert.ok(end !== -1 && end > start, 'could not find the "var EDGE_PX" boundary after fillVersion');
|
|
return SRC.slice(start, end);
|
|
}
|
|
|
|
// Fresh evaluation per case: versionPromise caches for the page lifetime, so a
|
|
// shared sandbox would hand the second case the first case's answer.
|
|
function load(fetchImpl) {
|
|
const sandbox = { fetch: fetchImpl, console };
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(sliceVersionFns() + '\nthis.fillVersion = fillVersion;', sandbox);
|
|
return sandbox.fillVersion;
|
|
}
|
|
|
|
function fakeLink() {
|
|
return { textContent: 'CoreScope', title: '', innerHTML: null };
|
|
}
|
|
|
|
// fillVersion does not return its promise, so settle the microtask queue.
|
|
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
|
|
(async () => {
|
|
console.log('\n=== #1985: the nav drawer version footer ===');
|
|
|
|
await test('a failing /api/health leaves the neutral label', async () => {
|
|
const el = fakeLink();
|
|
load(() => Promise.reject(new Error('offline')))(el);
|
|
await settle();
|
|
assert.strictEqual(el.textContent, 'CoreScope');
|
|
assert.strictEqual(el.innerHTML, null, 'nothing may be written as markup');
|
|
});
|
|
|
|
await test('a non-ok response leaves the neutral label', async () => {
|
|
const el = fakeLink();
|
|
load(() => Promise.resolve({ ok: false, json: () => Promise.resolve({}) }))(el);
|
|
await settle();
|
|
assert.strictEqual(el.textContent, 'CoreScope');
|
|
});
|
|
|
|
await test('a response without a version leaves the neutral label', async () => {
|
|
const el = fakeLink();
|
|
load(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ commit: 'abc1234' }) }))(el);
|
|
await settle();
|
|
assert.strictEqual(el.textContent, 'CoreScope',
|
|
'a health response missing "version" must not produce "CoreScope undefined"');
|
|
});
|
|
|
|
await test('a version is rendered, with commit and build time in the tooltip', async () => {
|
|
const el = fakeLink();
|
|
load(() => Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ version: 'v3.11.0', commit: 'a34244f4', buildTime: '2026-09-22T09:00:00Z' }),
|
|
}))(el);
|
|
await settle();
|
|
assert.strictEqual(el.textContent, 'CoreScope v3.11.0');
|
|
assert.ok(/a34244f4/.test(el.title), 'commit belongs in the tooltip: ' + el.title);
|
|
assert.ok(/2026-09-22/.test(el.title), 'build time belongs in the tooltip: ' + el.title);
|
|
});
|
|
|
|
await test('a version carrying markup is written as text, not HTML', async () => {
|
|
const el = fakeLink();
|
|
const hostile = '<img src=x onerror=alert(1)>';
|
|
load(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ version: hostile }) }))(el);
|
|
await settle();
|
|
assert.strictEqual(el.textContent, 'CoreScope ' + hostile,
|
|
'the string must land verbatim in textContent');
|
|
assert.strictEqual(el.innerHTML, null, 'innerHTML must never be touched');
|
|
});
|
|
|
|
await test('the health endpoint is requested once, however often the footer is filled', async () => {
|
|
let calls = 0;
|
|
const fill = load(() => { calls++; return Promise.resolve({ ok: true, json: () => Promise.resolve({ version: 'v1' }) }); });
|
|
fill(fakeLink()); fill(fakeLink()); fill(fakeLink());
|
|
await settle();
|
|
assert.strictEqual(calls, 1, 'the promise is cached for the page lifetime, got ' + calls + ' requests');
|
|
});
|
|
|
|
console.log(`\nTotal: ${passed} passed, ${failed} failed`);
|
|
if (failed) process.exit(1);
|
|
})();
|