Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions public/nav-drawer.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions public/nav-drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,6 +92,44 @@
'<use href="/icons/phosphor-sprite.svg#ph-' + name + '"></use></svg>';
}

// ── 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
Expand Down Expand Up @@ -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);

Expand All @@ -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).
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "═══════════════════════════════════════"
Expand Down
122 changes: 122 additions & 0 deletions test-issue-111-drawer-version-e2e.js
Original file line number Diff line number Diff line change
@@ -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.4<b>x</b>', 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.4<b>x</b>' && 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);
})();
Loading
Loading