Files
meshcore-analyzer/tests/unit/test-nav-drawer-version-footer.js
T
435ac25dd6 feat(nav): show the running version in the nav-drawer footer (#2068)
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>
2026-09-23 10:58:36 +02:00

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);
})();