// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later import assert from "node:assert/strict"; import test from "node:test"; import vm from "node:vm"; import { build } from "esbuild"; async function loadRoutes(window) { const result = await build({ entryPoints: [new URL("../src/features/navigation/routes.ts", import.meta.url).pathname], bundle: true, format: "cjs", platform: "browser", target: "es2022", write: false, }); const module = { exports: {} }; // The sandbox is bare; the browser globals the module actually uses go in. vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports, window, URLSearchParams }); return module.exports; } test("all top-level tabs have stable round-trip routes", async () => { const calls = []; const window = { location: { pathname: "/", search: "?rig=one", hash: "#panel" }, history: { pushState: (...args) => calls.push(["push", ...args]), replaceState: (...args) => calls.push(["replace", ...args]), }, }; const routes = await loadRoutes(window); for (const name of routes.TAB_ORDER) { assert.equal(routes.tabFromPath(routes.TAB_PATHS[name]), name); } routes.updateTabHistory("statistics"); assert.equal(calls[0][0], "push"); assert.equal(calls[0][3], "/statistics?rig=one#panel"); }); // A tune link is what one operator sends another: it has to survive being // typed by hand, and it has to come back out as the frequency that went in. test("tune links parse the frequencies people write", async () => { const routes = await loadRoutes({ location: { pathname: "/", search: "", hash: "" }, history: {} }); for (const [text, hz] of [ ["14074000", 14_074_000], ["14074000hz", 14_074_000], ["7074k", 7_074_000], ["14.074M", 14_074_000], ["1.2G", 1_200_000_000], [" 145500k ", 145_500_000], ]) { assert.equal(routes.parseFrequencyParam(text), hz, `${text} did not read as ${hz} Hz`); } for (const bad of ["", " ", "abc", "-7074", "0", "14,074", "1e6", null, undefined]) { assert.equal(routes.parseFrequencyParam(bad), null, `${bad} was read as a frequency`); } }); test("tune links carry rig, frequency, mode and bandwidth", async () => { const routes = await loadRoutes({ location: { pathname: "/", search: "", hash: "" }, history: {} }); assert.deepEqual({ ...routes.parseTuneLink("?rig=sdr&f=14.074M&mode=usb&bw=3k") }, { rig: "sdr", freqHz: 14_074_000, mode: "USB", bandwidthHz: 3000, }); // A link with nothing to say must not tune anything. assert.deepEqual({ ...routes.parseTuneLink("?tab=map") }, { rig: null, freqHz: null, mode: null, bandwidthHz: null, }); // Junk in a parameter is not a frequency, and must not become one. assert.deepEqual({ ...routes.parseTuneLink("?f=&mode=NOTAMODE&bw=wide") }, { rig: null, freqHz: null, mode: null, bandwidthHz: null, }); }); test("writing a tune link canonicalises it and leaves other parameters alone", async () => { const routes = await loadRoutes({ location: { pathname: "/", search: "", hash: "" }, history: {} }); const search = routes.tuneLinkSearch("?theme=dark&f=7074k", { rig: "sdr", freqHz: 14_074_000.4, mode: "usb", bandwidthHz: 3000, }); const params = new URLSearchParams(search); assert.equal(params.get("theme"), "dark", "an unrelated parameter was dropped"); assert.equal(params.get("f"), "14074000", "the frequency was not written as whole Hz"); assert.equal(params.get("mode"), "USB"); assert.equal(params.get("bw"), "3000"); assert.equal(params.get("rig"), "sdr"); // Round trip: what was written reads back as what went in. assert.deepEqual({ ...routes.parseTuneLink(search) }, { rig: "sdr", freqHz: 14_074_000, mode: "USB", bandwidthHz: 3000, }); // Nothing known yet means no stale tune parameters left behind. const cleared = routes.tuneLinkSearch(search, { rig: null, freqHz: null, mode: null, bandwidthHz: null }); assert.equal(cleared, "?theme=dark"); assert.equal(routes.tuneLinkSearch("", { rig: null, freqHz: null, mode: null, bandwidthHz: null }), ""); });