// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later // Geometry of the spectrum area while tuning. Everything above the spectrum is // driven by what happens to be in the visible range — band plan allocations, // bookmarks — and the strips that show them used to appear and disappear with // it, so tuning across a band edge moved the whole page under the operator's // cursor. Nothing else in the suite serves a rig with a spectrum. import assert from "node:assert/strict"; import { chromium } from "playwright-core"; import { startBrowser, startWebFixture } from "./web-fixture.mjs"; /* global document */ const BOOKMARKS = [ { id: "b1", name: "40m FT8", freq_hz: 7074000, mode: "DIG", category: "Digital", comment: "", locator: "" }, { id: "b2", name: "40m CW", freq_hz: 7030000, mode: "CW", category: "", comment: "", locator: "" }, ]; const BANDPLAN = { iaru1: { bands: [{ name: "40m", low_hz: 7000000, high_hz: 7200000, segments: [ { low_hz: 7000000, high_hz: 7040000, mode: "CW", label: "CW" }, { low_hz: 7040000, high_hz: 7200000, mode: "All", label: "All modes" }, ], }], }, }; // 40m has both bookmarks and allocations; 20m has neither. const BAND_WITH_CONTENT = 7074000; const BAND_WITHOUT_CONTENT = 14074000; const fixture = await startWebFixture({ spectrum: true, bookmarks: BOOKMARKS, bandplan: BANDPLAN, bandplanEnabled: true, }); const { browser, page, runtimeErrors } = await startBrowser(chromium); function readGeometry() { const top = (selector) => { const el = document.querySelector(selector); return el ? Math.round(el.getBoundingClientRect().top) : null; }; const axis = document.getElementById("spectrum-bookmark-axis"); const strip = document.getElementById("spectrum-bandplan-strip"); return { chips: axis.querySelectorAll(".spectrum-bookmark-chip").length, axisEmpty: axis.classList.contains("bm-axis-empty"), stripReserved: strip.classList.contains("bp-visible"), stripEmpty: strip.classList.contains("bp-empty"), overviewTop: top(".overview-strip"), spectrumTop: top("#spectrum-panel"), controlsTop: top(".controls-row"), footerTop: top(".footer"), docHeight: document.documentElement.scrollHeight, }; } const layoutOf = (geometry) => ({ overviewTop: geometry.overviewTop, spectrumTop: geometry.spectrumTop, controlsTop: geometry.controlsTop, footerTop: geometry.footerTop, docHeight: geometry.docHeight, }); async function tuneTo(hz) { fixture.setCenterHz(hz); await page.waitForTimeout(900); return page.evaluate(readGeometry); } try { await page.setViewportSize({ width: 1600, height: 950 }); await page.goto(fixture.origin, { waitUntil: "domcontentloaded" }); await page.locator("#content").waitFor({ state: "visible" }); await page.locator("#spectrum-panel").waitFor({ state: "visible" }); await page.waitForTimeout(1500); const populated = await tuneTo(BAND_WITH_CONTENT); assert.equal(populated.chips, BOOKMARKS.length, `expected both bookmarks, saw ${populated.chips}`); assert.equal(populated.axisEmpty, false, "bookmark rail claims to be empty with chips in it"); assert.equal(populated.stripReserved, true, "band plan strip is missing on a band with allocations"); assert.equal(populated.stripEmpty, false, "band plan strip claims to be empty with segments in it"); const bare = await tuneTo(BAND_WITHOUT_CONTENT); assert.equal(bare.chips, 0, `expected no bookmarks on ${BAND_WITHOUT_CONTENT}Hz, saw ${bare.chips}`); assert.equal(bare.axisEmpty, true, "bookmark rail should show its placeholder"); assert.equal(bare.stripReserved, true, "band plan strip gave its height back"); assert.equal(bare.stripEmpty, true, "band plan strip should be marked empty"); // The point of both placeholders: tuning must not move anything. assert.deepEqual(layoutOf(bare), layoutOf(populated), `tuning off the band moved the page: ${JSON.stringify(populated)} -> ${JSON.stringify(bare)}`); const back = await tuneTo(BAND_WITH_CONTENT); assert.equal(back.chips, BOOKMARKS.length, "bookmarks did not come back"); assert.deepEqual(layoutOf(back), layoutOf(populated), "tuning back moved the page"); // The rail covers the top of the overview, so only the chips may take // pointer events — the rest has to fall through to the plot behind it. const hits = await page.evaluate(() => { const chip = document.querySelector("#spectrum-bookmark-axis .spectrum-bookmark-chip"); const chipRect = chip.getBoundingClientRect(); const axisRect = document.getElementById("spectrum-bookmark-axis").getBoundingClientRect(); const onChip = document.elementFromPoint(chipRect.left + chipRect.width / 2, chipRect.top + chipRect.height / 2); const besideChip = document.elementFromPoint(axisRect.right - 30, axisRect.top + 10); return { chip: onChip?.closest(".spectrum-bookmark-chip") ? "chip" : (onChip?.id || onChip?.tagName), besideChip: besideChip?.id || besideChip?.tagName, }; }); assert.equal(hits.chip, "chip", `chip is not clickable, hit ${hits.chip}`); assert.equal(hits.besideChip, "overview-canvas", `rail swallows events, hit ${hits.besideChip}`); assert.deepEqual(runtimeErrors, []); } finally { await browser.close(); await fixture.close(); }