Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/navigation-routes.test.mjs
T
sjg a2c630a92b
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
[feat](trx-frontend-http): put the tuned frequency in the address bar
A receiver spreads by being linked to, and there was nothing to link to:
the routes carried the tab and nothing else, so "listen to this" could
only ever mean a screenshot and a frequency typed out in a message.

The query string now carries the dial -- rig, frequency, mode and
bandwidth -- in both directions.  Opening a link selects the rig, sets
the mode, tunes, then applies the bandwidth: a mode change brings its
own default bandwidth with it, so an explicit bw has to land after it.
Frequencies are read the way someone writes them by hand (7074k,
14.074M) and written back as whole Hz, so what comes out of the address
bar is the same link in canonical form.

After that the address bar keeps up with the dial, which is what makes
it copyable at any moment rather than only at load.  It is rewritten
with replaceState -- tuning is not navigation, and a swept dial would
otherwise bury the back button.  A link button in the top bar copies
the current link; it folds into the overflow menu when the bar is tight.

Applying a link changes the radio, so an rx session says so instead of
failing control calls one at a time.  A tab listening to a virtual
channel leaves the address alone rather than publishing a frequency the
rig is not on, and bw is skipped in both directions on rigs without
filter control, which would only refuse it.

The fixture pinned every state frame to 100 MHz plus jitter to keep
frames distinct, so no test could observe tuning at all.  The jitter
moves to the S-meter and the fixture echoes set_freq/set_mode/
set_bandwidth, as it already did for squelch.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-05 22:31:12 +02:00

94 lines
4.1 KiB
JavaScript

// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// 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 }), "");
});