Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/tune-links.mjs
T
sjgandClaude Opus 5 86dd36312e
CI / lint (pull_request) Successful in 2m20s
CI / test (pull_request) Successful in 8m32s
CI / frontend (pull_request) Successful in 4m27s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m26s
CI / test (push) Successful in 7m42s
CI / frontend (push) Successful in 3m35s
CI / reuse (push) Successful in 5s
[test](trx-frontend-http): type the frequency instead of filling it
tune-links drove the dial with Playwright's fill(), which writes a value
into the field without a keystroke.  The app arms its guard against its own
refreshes on the first keydown, so a filled field stays unguarded: any state
update landing between the fill and the Enter rewrites the field with the
frequency the radio is already on, and the Enter then re-applies that.  The
window is a few milliseconds wide on a developer's machine and wide enough
to lose on a loaded CI runner, where the test failed claiming the tuning had
landed on the frequency it started from.

Type it the way an operator does: select the field, then send the characters
as keystrokes.  The select arms the guard before a single character changes.

Under CPU throttling that reproduced the failure — 1 in 6 runs with fill(),
on this branch and on main alike — typing came through 8 runs clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-07 02:47:04 +02:00

116 lines
5.1 KiB
JavaScript

// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
// A link is how one operator tells another where to listen. Opening one has to
// put the radio there, and the address bar has to keep up with the dial after
// that — a link that goes stale the moment someone tunes is worse than none,
// because it looks like it works.
import assert from "node:assert/strict";
import { chromium } from "playwright-core";
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
/* global document, window */
const LINK_HZ = 14_074_000;
const LINK_BW = 2_800;
const TUNED_HZ = 7_040_000;
const fixture = await startWebFixture({ spectrum: true });
const { browser, page, runtimeErrors } = await startBrowser(chromium);
const dial = () => page.evaluate(() => ({
freqHz: window.lastFreqHz,
mode: document.getElementById("mode").value,
search: window.location.search,
path: window.location.pathname,
}));
// Typed, not filled. The app holds back its own refreshes of the frequency
// field from the first keystroke until Enter, so that a state update arriving
// mid-edit does not rewrite what is being typed. `fill()` sets the value
// without a keystroke, leaving the field unguarded: on a slow machine a state
// update could land between the fill and the Enter and put the old frequency
// back, and the Enter would then re-apply the frequency the radio was already
// on. Selecting first arms the guard before a single character changes.
async function tuneByHand(text) {
const field = page.locator("#freq");
await field.click();
await field.press("ControlOrMeta+a");
await field.pressSequentially(text);
await field.press("Enter");
}
try {
await page.setViewportSize({ width: 1500, height: 950 });
// Opening the link tunes the radio: frequency, mode and bandwidth.
await page.goto(`${fixture.origin}/?f=14.074M&mode=USB&bw=${LINK_BW}`, { waitUntil: "domcontentloaded" });
await page.locator("#content").waitFor({ state: "visible" });
await page.waitForTimeout(2500);
const opened = await dial();
assert.equal(opened.freqHz, LINK_HZ, `the link left the radio on ${opened.freqHz} Hz`);
assert.equal(opened.mode, "USB", `the link left the radio in ${opened.mode}`);
const bandwidth = await page.evaluate(() => window.currentBandwidthHz);
assert.equal(bandwidth, LINK_BW, `the link left the bandwidth at ${bandwidth} Hz`);
// Hand-written frequencies are canonicalised in place, so what the operator
// copies back out is the same link in the form the app writes.
const params = new URLSearchParams(opened.search);
assert.equal(params.get("f"), String(LINK_HZ), `the URL kept "${params.get("f")}"`);
assert.equal(params.get("mode"), "USB");
// Tuning by hand rewrites the link. This is the part that makes the address
// bar shareable at any moment rather than only at load.
await tuneByHand("7.040M");
await page.waitForTimeout(1500);
const tuned = await dial();
assert.equal(tuned.freqHz, TUNED_HZ, `tuning landed on ${tuned.freqHz} Hz`);
assert.equal(new URLSearchParams(tuned.search).get("f"), String(TUNED_HZ),
`the URL still reads "${tuned.search}" after tuning`);
// Tuning is not navigation: a swept dial must not bury the back button.
const historyLength = await page.evaluate(() => window.history.length);
await tuneByHand("7.100M");
await page.waitForTimeout(1200);
assert.equal(await page.evaluate(() => window.history.length), historyLength,
"tuning pushed a history entry instead of replacing one");
// Moving between tabs keeps the tune parameters: the path changes, the link
// does not stop being a link.
await page.locator('.tab[data-tab="map"]').click();
await page.waitForTimeout(800);
const onMap = await dial();
assert.equal(onMap.path, "/map", `the map tab landed on ${onMap.path}`);
assert.equal(new URLSearchParams(onMap.search).get("f"), "7100000",
`the map tab dropped the tune parameters: "${onMap.search}"`);
// A link naming a rig selects it before applying the rest.
await page.goto(`${fixture.origin}/?rig=rig-b&f=${TUNED_HZ}&mode=CW`, { waitUntil: "domcontentloaded" });
await page.locator("#content").waitFor({ state: "visible" });
await page.waitForTimeout(2500);
const switched = await page.evaluate(() => ({
rig: document.getElementById("header-rig-switch-select")?.value ?? null,
selected: window.location.search,
mode: document.getElementById("mode").value,
}));
assert.equal(switched.rig, "rig-b", `the link left the client on rig "${switched.rig}"`);
assert.equal(switched.mode, "CW", `the link left the radio in ${switched.mode}`);
// A link with nothing to say tunes nothing, and the address bar fills in
// with where the radio already is.
await page.goto(fixture.origin, { waitUntil: "domcontentloaded" });
await page.locator("#content").waitFor({ state: "visible" });
await page.waitForTimeout(2000);
const bare = await dial();
assert.equal(new URLSearchParams(bare.search).get("f"), String(bare.freqHz),
`a bare load wrote "${bare.search}" for a radio on ${bare.freqHz} Hz`);
assert.deepEqual(runtimeErrors, []);
} finally {
await browser.close();
await fixture.close();
}