diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a79c2112e..bb13fd5d6 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -159,6 +159,7 @@ jobs:
node test-channel-issue-1101.js
node test-observer-iata-1188.js
node test-pull-to-reconnect-1091.js
+ node test-issue-111-drawer-version.js
node test-channel-fluid-layout.js
node test-issue-1279-p2-code-filter.js
node test-area-filter.js
@@ -546,6 +547,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-path-inspector-coverage-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-resize-observer-leak-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-drawer-1064-e2e.js 2>&1 | tee -a e2e-output.txt
+ CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-111-drawer-version-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-audio-live-1297-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-audio-lab-1297-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-channel-decrypt-e2e.js 2>&1 | tee -a e2e-output.txt
diff --git a/public/nav-drawer.css b/public/nav-drawer.css
index 154f90ad7..dd556b299 100644
--- a/public/nav-drawer.css
+++ b/public/nav-drawer.css
@@ -160,6 +160,35 @@
white-space: nowrap;
}
+/* ── Footer: running version (#111) ────────────────────────────────────── */
+.nav-drawer-footer {
+ flex-shrink: 0; /* the list above scrolls; the footer stays at the bottom */
+ padding: 10px 16px 12px;
+ border-top: 1px solid var(--border);
+}
+
+.nav-drawer-version {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--nav-text-muted);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ text-decoration: none;
+}
+
+.nav-drawer-version:hover,
+.nav-drawer-version:focus-visible {
+ color: var(--nav-text);
+ text-decoration: underline;
+}
+
+.nav-drawer-version:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
@media (prefers-reduced-motion: reduce) {
.nav-drawer-item,
.nav-drawer-close {
diff --git a/public/nav-drawer.js b/public/nav-drawer.js
index 4d59534ea..59017b509 100644
--- a/public/nav-drawer.js
+++ b/public/nav-drawer.js
@@ -39,6 +39,7 @@
var wired = false;
var drawerEl = null;
var backdropEl = null;
+ var versionEl = null;
var dragging = false;
var startX = 0;
var startY = 0;
@@ -91,6 +92,44 @@
'';
}
+ // ── Version footer (#111) ───────────────────────────────────────────────
+ // GET /api/health reports {version, commit, buildTime}; the server fills
+ // any it cannot resolve with "unknown". Fetched on the first open that
+ // passes the width gate (never at page load: the drawer may never open,
+ // and cannot at <= NARROW_MAX), then cached for the page lifetime --
+ // failures included -- so re-opening adds no requests. Values are written
+ // with textContent / title only.
+ var RELEASES_URL = 'https://github.com/dborup/CoreScope/releases';
+ var versionRequested = false;
+
+ function healthField(h, k) {
+ var v = h && h[k];
+ if (typeof v !== 'string') return '';
+ v = v.trim();
+ return (v && v.toLowerCase() !== 'unknown') ? v : '';
+ }
+
+ function applyVersion(el, h) {
+ var version = healthField(h, 'version');
+ if (!version) return; // keep the neutral "CoreScope" label
+ el.textContent = 'CoreScope ' + version;
+ var bits = [];
+ var commit = healthField(h, 'commit');
+ var built = healthField(h, 'buildTime');
+ if (commit) bits.push('commit ' + commit);
+ if (built) bits.push('built ' + built);
+ if (bits.length) el.title = bits.join(' \u00B7 ');
+ }
+
+ function requestVersion() {
+ if (versionRequested || !versionEl || typeof fetch !== 'function') return;
+ versionRequested = true;
+ var el = versionEl;
+ fetch('/api/health', { headers: { Accept: 'application/json' } })
+ .then(function (r) { return r && r.ok ? r.json() : null; })
+ .then(function (h) { applyVersion(el, h); }, function () { /* neutral label */ });
+ }
+
var EDGE_PX = 44; // pointerdown must start within left N px (drawer trigger zone)
var EDGE_MIN_PX = 24; // first N px reserved for iOS Safari back-swipe (do not claim)
var NARROW_MAX = 768; // Option A: disabled at ≤ this width
@@ -203,6 +242,19 @@
renderRoutes(list);
drawerEl.appendChild(list);
+ var footer = document.createElement('div');
+ footer.className = 'nav-drawer-footer';
+ var ver = document.createElement('a');
+ ver.className = 'nav-drawer-version';
+ ver.setAttribute('data-nav-drawer-version', '');
+ ver.setAttribute('href', RELEASES_URL);
+ ver.setAttribute('target', '_blank');
+ ver.setAttribute('rel', 'noopener noreferrer');
+ ver.textContent = 'CoreScope';
+ footer.appendChild(ver);
+ drawerEl.appendChild(footer);
+ versionEl = ver;
+
document.body.appendChild(backdropEl);
document.body.appendChild(drawerEl);
@@ -229,6 +281,7 @@
function open() {
buildDom();
if (!isWide()) return; // Option A
+ requestVersion(); // #111: first open only
if (!drawerWidth) drawerWidth = drawerEl.getBoundingClientRect().width || 320;
// Capture the previously-focused element BEFORE we move focus, so close()
// can restore it. Guard against opening twice (don't overwrite on re-open).
diff --git a/test-all.sh b/test-all.sh
index 5c4e9bafb..6aa09c17c 100755
--- a/test-all.sh
+++ b/test-all.sh
@@ -110,6 +110,7 @@ node test-network-digest-tool.js
node test-position-gaps-tool.js
node test-gps-sanity-tool.js
node test-map-scope-filter.js
+node test-issue-111-drawer-version.js
echo ""
echo "═══════════════════════════════════════"
diff --git a/test-issue-111-drawer-version-e2e.js b/test-issue-111-drawer-version-e2e.js
new file mode 100644
index 000000000..1aa8c0ffc
--- /dev/null
+++ b/test-issue-111-drawer-version-e2e.js
@@ -0,0 +1,122 @@
+/**
+ * E2E (#111): running version in the nav-drawer footer.
+ *
+ * At drawer viewports (> 768px, incl. a short one) the footer sits at the
+ * bottom of the drawer, inside it, below the route list, which keeps every
+ * route reachable. /api/health is requested only on the first open and never
+ * again; at a narrow width (drawer disabled) nothing is requested. With the
+ * local server's "unknown" version the label stays "CoreScope"; with a routed
+ * response it shows the version as text.
+ *
+ * Usage: BASE_URL=http://localhost:13581 node test-issue-111-drawer-version-e2e.js
+ */
+'use strict';
+const { chromium } = require('playwright');
+
+const BASE = process.env.BASE_URL || 'http://localhost:13581';
+let passed = 0, failed = 0;
+async function step(name, fn) {
+ try { await fn(); passed++; console.log(' ✓ ' + name); }
+ catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
+}
+function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
+
+async function page(browser, viewport, errors) {
+ const ctx = await browser.newContext({ viewport });
+ const p = await ctx.newPage();
+ p.setDefaultTimeout(15000);
+ const health = [];
+ p.on('request', (r) => { if (new URL(r.url()).pathname === '/api/health') health.push(r.url()); });
+ p.on('pageerror', (e) => errors.push(e.message));
+ await p.goto(BASE + '/#/packets');
+ await p.waitForFunction(() => !!window.__navDrawer && !!document.querySelector('[data-nav-drawer]'));
+ await p.waitForTimeout(500);
+ return { ctx, p, health };
+}
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ executablePath: process.env.CHROMIUM_PATH || undefined,
+ args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
+ });
+ const errors = [];
+ console.log('\n=== #111 nav-drawer version footer against ' + BASE + ' ===');
+
+ for (const vp of [{ width: 1440, height: 900 }, { width: 1024, height: 768 }, { width: 800, height: 900 }, { width: 1280, height: 480 }]) {
+ await step(`${vp.width}x${vp.height}: footer inside the drawer, below a reachable route list; one request`, async () => {
+ const { ctx, p, health } = await page(browser, vp, errors);
+ try {
+ assert(health.length === 0, health.length + ' /api/health requests before the drawer was opened');
+ await p.evaluate(() => window.__navDrawer.open());
+ await p.waitForTimeout(400);
+ const r = await p.evaluate(() => {
+ const d = document.querySelector('[data-nav-drawer]');
+ const a = d.querySelector('[data-nav-drawer-version]');
+ if (!a) return null;
+ const dr = d.getBoundingClientRect(), fr = a.closest('.nav-drawer-footer').getBoundingClientRect();
+ const list = d.querySelector('.nav-drawer-list');
+ const lr = list.getBoundingClientRect();
+ const items = [...list.querySelectorAll('[data-nav-drawer-item]')];
+ const last = items[items.length - 1];
+ last.scrollIntoView({ block: 'nearest' });
+ const lastR = last.getBoundingClientRect();
+ const hit = document.elementFromPoint(lastR.left + 10, lastR.top + lastR.height / 2);
+ return {
+ text: a.textContent, href: a.getAttribute('href'),
+ inside: fr.left >= dr.left - 1 && fr.right <= dr.right + 1 && fr.bottom <= dr.bottom + 1 && fr.top >= dr.top,
+ belowList: fr.top >= lr.bottom - 1,
+ lastReachable: last.contains(hit),
+ footerVisible: fr.height > 0 && getComputedStyle(a).visibility !== 'hidden',
+ };
+ });
+ assert(r, 'no version link in the drawer');
+ assert(/^CoreScope( \S.*)?$/.test(r.text) && !/undefined|null|unknown/i.test(r.text), 'label: ' + r.text);
+ assert(r.href === 'https://github.com/dborup/CoreScope/releases', 'href: ' + r.href);
+ assert(r.inside && r.footerVisible, 'footer not inside the drawer: ' + JSON.stringify(r));
+ assert(r.belowList, 'footer overlaps the route list');
+ assert(r.lastReachable, 'the last route is covered by the footer');
+ await p.evaluate(() => { window.__navDrawer.close(); window.__navDrawer.open(); window.__navDrawer.close(); window.__navDrawer.open(); });
+ await p.waitForTimeout(300);
+ assert(health.length === 1, health.length + ' /api/health requests after opening four times');
+ } finally { await ctx.close(); }
+ });
+ }
+
+ await step('at a narrow width (drawer disabled) opening requests nothing', async () => {
+ const { ctx, p, health } = await page(browser, { width: 700, height: 900 }, errors);
+ try {
+ await p.evaluate(() => { window.__navDrawer.open(); window.__navDrawer.open(); });
+ await p.waitForTimeout(400);
+ assert(health.length === 0, health.length + ' requests at 700px');
+ } finally { await ctx.close(); }
+ });
+
+ await step('a routed health response shows the version as text with a tooltip', async () => {
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
+ const p = await ctx.newPage();
+ p.on('pageerror', (e) => errors.push(e.message));
+ await p.route('**/api/health', (route) => route.fulfill({
+ status: 200, contentType: 'application/json',
+ body: JSON.stringify({ version: 'v3.1.4x', commit: 'abc1234', buildTime: '2026-09-01T10:00:00Z' }),
+ }));
+ try {
+ await p.goto(BASE + '/#/packets');
+ await p.waitForFunction(() => !!window.__navDrawer);
+ await p.evaluate(() => window.__navDrawer.open());
+ await p.waitForFunction(() => /v3/.test((document.querySelector('[data-nav-drawer-version]') || {}).textContent || ''));
+ const r = await p.evaluate(() => {
+ const a = document.querySelector('[data-nav-drawer-version]');
+ return { text: a.textContent, title: a.title, kids: a.children.length };
+ });
+ assert(r.text === 'CoreScope v3.1.4x' && r.kids === 0, JSON.stringify(r));
+ assert(/abc1234/.test(r.title) && /2026-09-01/.test(r.title), 'tooltip: ' + r.title);
+ } finally { await ctx.close(); }
+ });
+
+ await step('no page errors', async () => { assert(errors.length === 0, errors.join(' | ')); });
+
+ await browser.close();
+ console.log('\n--- ' + passed + ' passed, ' + failed + ' failed ---');
+ process.exit(failed ? 1 : 0);
+})();
diff --git a/test-issue-111-drawer-version.js b/test-issue-111-drawer-version.js
new file mode 100644
index 000000000..4f06feb61
--- /dev/null
+++ b/test-issue-111-drawer-version.js
@@ -0,0 +1,227 @@
+/* test-issue-111-drawer-version.js — running version in the nav-drawer footer (#111).
+ *
+ * Loads the real public/nav-drawer.js in a vm with a small fake DOM and a
+ * counting fetch stub. Checks: footer label + fork releases link; no
+ * /api/health request at page load or while the drawer is gated off
+ * (narrow viewport); one request on the first wide open and none on
+ * re-open; success fills "CoreScope " and a commit/build tooltip;
+ * rejected, non-OK, invalid-JSON, version-less and "unknown" responses keep
+ * the neutral "CoreScope"; remote values only ever go through textContent.
+ */
+'use strict';
+const vm = require('vm');
+const fs = require('fs');
+const assert = require('assert');
+
+console.log('--- test-issue-111-drawer-version.js ---');
+let passed = 0, failed = 0;
+const pending = [];
+function test(name, fn) {
+ pending.push(async () => {
+ try { await fn(); passed++; console.log(' ✅ ' + name); }
+ catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
+ });
+}
+
+const SRC = fs.readFileSync(__dirname + '/public/nav-drawer.js', 'utf8');
+const RELEASES = 'https://github.com/dborup/CoreScope/releases';
+
+// ── minimal DOM ───────────────────────────────────────────────────────────
+function makeDoc() {
+ let htmlWrites = 0;
+ function El(tag) {
+ this.tagName = String(tag).toUpperCase();
+ this.children = [];
+ this.parentNode = null;
+ this.attrs = {};
+ this.style = {};
+ this.dataset = {};
+ this._text = '';
+ this._html = '';
+ this.hidden = false;
+ this.title = '';
+ this.className = '';
+ const self = this;
+ this.classList = {
+ add(c) { const s = new Set(self.className.split(/\s+/).filter(Boolean)); s.add(c); self.className = [...s].join(' '); },
+ remove(c) { self.className = self.className.split(/\s+/).filter((x) => x && x !== c).join(' '); },
+ contains(c) { return self.className.split(/\s+/).includes(c); },
+ toggle(c) { if (this.contains(c)) this.remove(c); else this.add(c); },
+ };
+ }
+ El.prototype = {
+ appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
+ removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parentNode = null; return c; },
+ get firstChild() { return this.children[0] || null; },
+ setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'href') this._href = String(v); },
+ getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
+ removeAttribute(k) { delete this.attrs[k]; },
+ hasAttribute(k) { return k in this.attrs; },
+ addEventListener() {}, removeEventListener() {},
+ focus() {}, getBoundingClientRect() { return { left: 0, right: 320, width: 320, top: 0, bottom: 800 }; },
+ contains(n) { for (let x = n; x; x = x.parentNode) if (x === this) return true; return false; },
+ get textContent() { return this._text + this.children.map((c) => c.textContent).join(''); },
+ set textContent(v) { this.children = []; this._text = String(v); },
+ get innerHTML() { return this._html; },
+ set innerHTML(v) { htmlWrites++; this._html = String(v); this.children = []; },
+ get href() { return this._href || this.attrs.href || ''; },
+ set href(v) { this._href = String(v); this.attrs.href = String(v); },
+ querySelector(sel) { return this.querySelectorAll(sel)[0] || null; },
+ querySelectorAll(sel) {
+ const parts = sel.split(',').map((s) => s.trim());
+ const out = [];
+ (function walk(n) {
+ for (const c of n.children) {
+ if (parts.some((p) => matches(c, p))) out.push(c);
+ walk(c);
+ }
+ })(this);
+ return out;
+ },
+ };
+ function matches(el, sel) {
+ const m = sel.match(/^([a-z]*)(?:\.([\w-]+))?(?:\[([\w-]+)(?:="([^"]*)")?\])?/i);
+ if (!m) return false;
+ if (m[1] && el.tagName !== m[1].toUpperCase()) return false;
+ if (m[2] && !el.classList.contains(m[2])) return false;
+ if (m[3] && !(m[3] in el.attrs) && !(m[3] === 'href' && el._href)) return false;
+ if (m[4] != null && el.attrs[m[3]] !== m[4]) return false;
+ return !!(m[1] || m[2] || m[3]);
+ }
+ const body = new El('body');
+ const doc = {
+ readyState: 'complete', body, activeElement: body,
+ createElement: (t) => new El(t),
+ addEventListener() {}, removeEventListener() {},
+ querySelector: (s) => body.querySelector(s),
+ querySelectorAll: (s) => body.querySelectorAll(s),
+ };
+ return { doc, htmlWrites: () => htmlWrites };
+}
+
+// ── harness ───────────────────────────────────────────────────────────────
+function load(opts) {
+ opts = opts || {};
+ const { doc, htmlWrites } = makeDoc();
+ const calls = [];
+ let narrow = !!opts.narrow;
+ const win = {
+ innerWidth: narrow ? 700 : 1280,
+ matchMedia: (q) => ({ get matches() { return /max-width/.test(q) ? narrow : false; }, addEventListener() {} }),
+ };
+ const ctx = {
+ window: win, document: doc, console,
+ requestAnimationFrame: (fn) => { fn(); return 1; },
+ performance: { now: () => 0 },
+ Promise, JSON, Object, Array, String, Number, Math, Error, Set,
+ fetch: (url, init) => { calls.push(url); return opts.fetch ? opts.fetch(url, init) : new Promise(() => {}); },
+ };
+ win.fetch = ctx.fetch;
+ vm.createContext(ctx);
+ vm.runInContext(SRC, ctx);
+ const drawer = () => doc.body.querySelector('[data-nav-drawer]');
+ return {
+ calls, doc, htmlWrites, win,
+ api: win.__navDrawer,
+ setNarrow(v) { narrow = v; win.innerWidth = v ? 700 : 1280; },
+ footerLink: () => drawer() && drawer().querySelector('[data-nav-drawer-version]'),
+ };
+}
+const flush = () => new Promise((r) => setTimeout(r, 0));
+const ok = (body) => () => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(body) });
+
+// ── tests ─────────────────────────────────────────────────────────────────
+test('the footer link is built with the neutral label and the fork releases URL, without a request', async () => {
+ const b = load();
+ await flush();
+ const a = b.footerLink();
+ assert(a, 'no [data-nav-drawer-version] footer link in the drawer');
+ assert.strictEqual(a.tagName, 'A');
+ assert.strictEqual(a.textContent, 'CoreScope');
+ assert.strictEqual(a.href, RELEASES);
+ assert.strictEqual(a.getAttribute('target') || a.target, '_blank');
+ assert(/noopener/.test(a.getAttribute('rel') || a.rel || ''), 'rel lacks noopener');
+ assert(a.parentNode && a.parentNode.classList.contains('nav-drawer-footer'), 'link is not inside .nav-drawer-footer');
+ assert.strictEqual(b.calls.length, 0, 'fetched at page load');
+});
+
+test('no request while the drawer is gated off at narrow widths', async () => {
+ const b = load({ narrow: true, fetch: ok({ version: 'v1.2.3' }) });
+ b.api.open(); b.api.open();
+ await flush();
+ assert.strictEqual(b.calls.length, 0, b.calls.length + ' requests at a narrow width');
+});
+
+test('the first wide open fetches /api/health once and fills version and tooltip', async () => {
+ const b = load({ fetch: ok({ version: 'v1.2.3', commit: 'abc1234', buildTime: '2026-09-01T10:00:00Z' }) });
+ b.api.open();
+ await flush(); await flush();
+ assert.deepStrictEqual(b.calls.slice(), ['/api/health']);
+ const a = b.footerLink();
+ assert.strictEqual(a.textContent, 'CoreScope v1.2.3');
+ assert(a.title.includes('abc1234') && a.title.includes('2026-09-01T10:00:00Z'), 'tooltip: ' + a.title);
+});
+
+test('re-opening (also after narrowing and widening) adds no requests', async () => {
+ const b = load({ fetch: ok({ version: 'v1.2.3' }) });
+ b.api.open(); b.api.close();
+ await flush();
+ b.api.open(); b.api.close(); b.api.toggle(); b.api.toggle();
+ b.setNarrow(true); b.api.open(); b.setNarrow(false); b.api.open();
+ await flush(); await flush();
+ assert.strictEqual(b.calls.length, 1, b.calls.length + ' requests');
+ assert.strictEqual(b.footerLink().textContent, 'CoreScope v1.2.3');
+});
+
+const neutral = {
+ 'a rejected fetch': () => Promise.reject(new Error('offline')),
+ 'a non-OK response': () => Promise.resolve({ ok: false, status: 503, json: () => Promise.resolve({ version: 'v9' }) }),
+ 'invalid JSON': () => Promise.resolve({ ok: true, status: 200, json: () => Promise.reject(new SyntaxError('bad')) }),
+ 'a missing version': ok({ commit: 'abc' }),
+ 'an empty version': ok({ version: ' ' }),
+ 'the server placeholder "unknown"': ok({ version: 'unknown', commit: 'unknown', buildTime: 'unknown' }),
+ 'a non-string version': ok({ version: { toString() { return 'x'; } } }),
+ 'a null body': ok(null),
+};
+for (const [what, fetch] of Object.entries(neutral)) {
+ test(what + ' keeps the neutral "CoreScope" label and no tooltip, and is not retried', async () => {
+ const b = load({ fetch });
+ b.api.open();
+ await flush(); await flush();
+ const a = b.footerLink();
+ assert.strictEqual(a.textContent, 'CoreScope');
+ assert(!/undefined|null|unknown|object/i.test(a.textContent + ' ' + a.title), 'label/tooltip: ' + a.textContent + ' / ' + a.title);
+ b.api.close(); b.api.open();
+ await flush();
+ assert.strictEqual(b.calls.length, 1, 'the failed request was retried on re-open');
+ });
+}
+
+test('"unknown" commit/build are left out of the tooltip', async () => {
+ const b = load({ fetch: ok({ version: 'v2', commit: 'unknown', buildTime: '2026-09-01' }) });
+ b.api.open();
+ await flush(); await flush();
+ const a = b.footerLink();
+ assert.strictEqual(a.textContent, 'CoreScope v2');
+ assert(!/unknown/.test(a.title) && /2026-09-01/.test(a.title), 'tooltip: ' + a.title);
+});
+
+test('markup-shaped health values are rendered as text, never parsed as HTML', async () => {
+ const evil = '
';
+ const b = load({ fetch: ok({ version: evil, commit: 'c', buildTime: '">' }) });
+ const before = b.htmlWrites();
+ b.api.open();
+ await flush(); await flush();
+ const a = b.footerLink();
+ assert.strictEqual(a.textContent, 'CoreScope ' + evil);
+ assert.strictEqual(a.children.length, 0, 'the version created child elements');
+ assert.strictEqual(a.innerHTML, '', 'innerHTML was used on the footer link');
+ // opening the drawer writes no HTML except the static route icons
+ assert.strictEqual(b.htmlWrites(), before, 'open() wrote HTML after the health response');
+});
+
+(async () => {
+ for (const t of pending) await t();
+ console.log(`\n${passed} passed, ${failed} failed`);
+ if (failed > 0) process.exit(1);
+})();