From 038ecfa2dd1fb62f1302318e1e6d44c9ea245705 Mon Sep 17 00:00:00 2001 From: you Date: Tue, 24 Mar 2026 01:19:56 +0000 Subject: [PATCH] Add 41 frontend helper unit tests (app.js, nodes.js, hop-resolver.js) Test pure functions from frontend JS files using vm.createContext sandbox: - timeAgo: null/undefined handling, seconds/minutes/hours/days formatting - escapeHtml: XSS chars, null input, type coercion - routeTypeName/payloadTypeName: known types + unknown fallback - truncate: short/long/null strings - getStatusTooltip: role-specific threshold messages - getStatusInfo: active/stale status for repeaters and companions - renderNodeBadges: HTML output contains role badge - sortNodes: returns sorted array - HopResolver: init/ready, single/ambiguous/unknown prefix resolution, geo disambiguation with origin anchor, IATA regional filtering Note: c8 coverage doesn't track vm.runInContext-evaluated code, so these don't improve the c8 coverage numbers. The tests still validate correctness of frontend logic in CI. --- test-all.sh | 1 + test-frontend-helpers.js | 372 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 test-frontend-helpers.js diff --git a/test-all.sh b/test-all.sh index 3c61942..8fdf58c 100755 --- a/test-all.sh +++ b/test-all.sh @@ -14,6 +14,7 @@ node test-decoder-spec.js node test-packet-store.js node test-packet-filter.js node test-aging.js +node test-frontend-helpers.js node test-regional-filter.js node test-server-helpers.js node test-db.js diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js new file mode 100644 index 0000000..181eb16 --- /dev/null +++ b/test-frontend-helpers.js @@ -0,0 +1,372 @@ +/* Unit tests for frontend helper functions (tested via VM sandbox) */ +'use strict'; +const vm = require('vm'); +const fs = require('fs'); +const assert = require('assert'); + +let passed = 0, failed = 0; +function test(name, fn) { + try { fn(); passed++; console.log(` ✅ ${name}`); } + catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); } +} + +// --- Build a browser-like sandbox --- +function makeSandbox() { + const ctx = { + window: { addEventListener: () => {}, dispatchEvent: () => {} }, + document: { + readyState: 'complete', + createElement: () => ({ id: '', textContent: '', innerHTML: '' }), + head: { appendChild: () => {} }, + getElementById: () => null, + addEventListener: () => {}, + querySelectorAll: () => [], + querySelector: () => null, + }, + console, + Date, + Infinity, + Math, + Array, + Object, + String, + Number, + JSON, + RegExp, + Error, + TypeError, + parseInt, + parseFloat, + isNaN, + isFinite, + encodeURIComponent, + decodeURIComponent, + setTimeout: () => {}, + clearTimeout: () => {}, + setInterval: () => {}, + clearInterval: () => {}, + fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }), + performance: { now: () => Date.now() }, + localStorage: (() => { + const store = {}; + return { + getItem: k => store[k] || null, + setItem: (k, v) => { store[k] = String(v); }, + removeItem: k => { delete store[k]; }, + }; + })(), + location: { hash: '' }, + CustomEvent: class CustomEvent {}, + Map, + Promise, + URLSearchParams, + addEventListener: () => {}, + dispatchEvent: () => {}, + requestAnimationFrame: (cb) => setTimeout(cb, 0), + }; + vm.createContext(ctx); + return ctx; +} + +function loadInCtx(ctx, file) { + vm.runInContext(fs.readFileSync(file, 'utf8'), ctx); + // Copy window.* to global context so bare references work + for (const k of Object.keys(ctx.window)) { + ctx[k] = ctx.window[k]; + } +} + +// ===== APP.JS TESTS ===== +console.log('\n=== app.js: timeAgo ==='); +{ + const ctx = makeSandbox(); + loadInCtx(ctx, 'public/roles.js'); + loadInCtx(ctx, 'public/app.js'); + const timeAgo = ctx.timeAgo; + + test('null returns dash', () => assert.strictEqual(timeAgo(null), '—')); + test('undefined returns dash', () => assert.strictEqual(timeAgo(undefined), '—')); + test('empty string returns dash', () => assert.strictEqual(timeAgo(''), '—')); + + test('30 seconds ago', () => { + const d = new Date(Date.now() - 30000).toISOString(); + assert.strictEqual(timeAgo(d), '30s ago'); + }); + test('5 minutes ago', () => { + const d = new Date(Date.now() - 300000).toISOString(); + assert.strictEqual(timeAgo(d), '5m ago'); + }); + test('2 hours ago', () => { + const d = new Date(Date.now() - 7200000).toISOString(); + assert.strictEqual(timeAgo(d), '2h ago'); + }); + test('3 days ago', () => { + const d = new Date(Date.now() - 259200000).toISOString(); + assert.strictEqual(timeAgo(d), '3d ago'); + }); +} + +console.log('\n=== app.js: escapeHtml ==='); +{ + const ctx = makeSandbox(); + loadInCtx(ctx, 'public/roles.js'); + loadInCtx(ctx, 'public/app.js'); + const escapeHtml = ctx.escapeHtml; + + test('escapes < and >', () => assert.strictEqual(escapeHtml('