// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later import assert from "node:assert/strict"; import { chromium } from "playwright-core"; import { startBrowser, startWebFixture } from "./web-fixture.mjs"; // page.evaluate callbacks run in the browser, not in this Node process. /* global document, getComputedStyle, window, location */ const fixture = await startWebFixture(); const { selectedRigs } = fixture; const { browser, page, runtimeErrors } = await startBrowser(chromium); try { await page.goto(`${fixture.origin}/`, { waitUntil: "domcontentloaded" }); await page.waitForTimeout(500); assert.deepEqual(runtimeErrors, []); await page.locator("#content").waitFor({ state: "visible" }); assert.equal(await page.locator("#auth-gate").isVisible(), false); assert.equal(await page.locator("#tab-main").isVisible(), true); await page.locator("summary", { hasText: "Audio controls" }).click(); assert.equal(await page.locator("#rx-audio-btn").count(), 1); // Digital modes: the decoders are a list down the side, and the panel for the // selected one sits beside it. A horizontal strip put thirteen decoders in a // scroller and marked the open one with a single underline among them. await page.locator('.tab[data-tab="digital-modes"]').click(); await page.locator("#tab-digital-modes").waitFor({ state: "visible" }); await page.locator('.sub-tab[data-subtab="ft8"]').click(); await page.waitForTimeout(250); const digital = await page.evaluate(() => { const bar = document.querySelector("#tab-digital-modes > .sub-tab-bar").getBoundingClientRect(); const panel = document.getElementById("subtab-ft8").getBoundingClientRect(); const shown = [...document.querySelectorAll("#tab-digital-modes > .sub-tab-panel")] .filter((element) => getComputedStyle(element).display !== "none") .map((element) => element.id); return { sidebarIsColumn: bar.height > bar.width, panelBesideSidebar: Math.round(panel.left) >= Math.round(bar.right), panelsShown: shown, decoders: document.querySelectorAll("#tab-digital-modes > .sub-tab-bar .sub-tab").length, }; }); assert.ok(digital.sidebarIsColumn, "the decoder list is not a sidebar"); assert.ok(digital.panelBesideSidebar, "the decoder panel does not sit beside the sidebar"); assert.deepEqual(digital.panelsShown, ["subtab-ft8"], `panels shown: ${JSON.stringify(digital.panelsShown)}`); assert.ok(digital.decoders >= 10, `only ${digital.decoders} decoders in the sidebar`); // Each decoder's list fills its panel. FT8, FT4, FT2 and WSPR size against // the panel with flex, so a panel sized to its own content collapsed them to // their 120px minimum with the rest of the page left empty; the marine lists // were sized by a viewport formula that stopped matching when the panel // changed shape. for (const [subtab, list] of [["ft8", "ft8-messages"], ["wspr", "wspr-messages"], ["ais", "ais-messages"], ["aprs", "aprs-packets"], ["hf-aprs", "hf-aprs-packets"]]) { await page.locator(`.sub-tab[data-subtab="${subtab}"]`).click(); await page.waitForTimeout(150); const filled = await page.evaluate((id) => { const element = document.getElementById(id); const panel = element.closest(".sub-tab-panel"); return { list: Math.round(element.getBoundingClientRect().height), panel: Math.round(panel.getBoundingClientRect().height), scrolls: getComputedStyle(element).overflowY, }; }, list); assert.ok(filled.list > filled.panel * 0.6, `${subtab}: the list is ${filled.list}px in a ${filled.panel}px panel`); assert.equal(filled.scrolls, "auto", `${subtab}: the list does not scroll on its own`); } await page.locator('.tab[data-tab="main"]').click(); await page.waitForTimeout(200); // Map links from decode rows, before anything has opened the Map tab. The // lazy map module used to install these globals itself, so an AIS pin threw // "not a function" and an APRS link silently did nothing until the tab had // been visited once. const mapLinkReady = await page.evaluate(() => ({ position: typeof window.navigateToAprsMap, locator: typeof window.navigateToMapLocator, mapModuleLoaded: !!window.trx.modules.map, })); assert.equal(mapLinkReady.position, "function", "navigateToAprsMap is missing before the map loads"); assert.equal(mapLinkReady.locator, "function", "navigateToMapLocator is missing before the map loads"); assert.equal(mapLinkReady.mapModuleLoaded, false, "the map module was already loaded, so this proves nothing"); await page.evaluate(() => { window.navigateToAprsMap(52.2, 21.0); }); await page.locator("#aprs-map .leaflet-pane").first().waitFor({ state: "attached" }); await page.waitForTimeout(500); const followed = await page.evaluate(() => ({ path: location.pathname, active: [...document.querySelectorAll(".tab-bar .tab.active")].map((tab) => tab.dataset.tab || tab.id), mapHeight: Math.round(document.getElementById("aprs-map").getBoundingClientRect().height), })); assert.equal(followed.path, "/map", `the map link left the page on ${followed.path}`); assert.ok(followed.active.includes("map"), `the strip marks ${JSON.stringify(followed.active)}`); assert.ok(followed.mapHeight > 100, `the map came up ${followed.mapHeight}px tall`); await page.locator('.tab[data-tab="main"]').click(); await page.waitForTimeout(200); // Section order in the tray. "Advanced radio controls" is built at runtime, // so it lands wherever ui-core puts it rather than where the markup says — // appending, as it once did, always left it last. const sections = await page.evaluate(() => [...document.querySelectorAll(".controls-tray > details")].map((section) => section.querySelector("summary").textContent.trim())); assert.deepEqual(sections, ["Advanced radio controls", "Audio controls", "Scheduler controls"], `tray sections are ${JSON.stringify(sections)}`); // Mode is a button group over a hidden