// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later // The station log: the panel opens an entry pre-filled from the radio and the // server's clock, writes a contact, keeps it, and says whether the station has // been worked before. And the ham layout, which is offered only where a rig can // transmit -- a log is of contacts made. import assert from "node:assert/strict"; import { chromium } from "playwright-core"; import { startBrowser, startWebFixture } from "./web-fixture.mjs"; /* global document, window, location */ const HELD = [ { id: "q1", started_at: "2026-08-06T14:22:00Z", call: "DL3ABC", freq_hz: 14_074_000, band: "20m", mode: "FT8", rst_sent: "-07", rst_rcvd: "-12", gridsquare: "JO31", my_rig: "Primary fixture", }, { id: "q2", started_at: "2026-08-06T13:05:00Z", call: "SP9XYZ", freq_hz: 7_120_000, band: "40m", mode: "SSB", submode: "LSB", rst_sent: "59", rst_rcvd: "57", gridsquare: "JO90", my_rig: "Primary fixture", }, ]; const fixture = await startWebFixture({ spectrum: true, tx: true, logbook: [...HELD] }); const { browser, page, runtimeErrors } = await startBrowser(chromium); const readPanel = () => page.evaluate(() => ({ rows: [...(document.getElementById("log-rows")?.children ?? [])].map((row) => ({ call: row.querySelector(".log-cell-call")?.textContent ?? "", cells: [...row.querySelectorAll("td")].map((cell) => cell.textContent), })), summary: document.getElementById("log-summary")?.textContent ?? "", freq: document.getElementById("log-freq")?.value ?? "", mode: document.getElementById("log-mode")?.value ?? "", call: document.getElementById("log-call")?.value ?? "", grid: document.getElementById("log-grid")?.value ?? "", rig: document.getElementById("log-station-rig-name")?.textContent ?? "", clock: document.getElementById("log-clock")?.textContent ?? "", worked: document.getElementById("log-worked-before")?.textContent ?? "", })); try { await page.setViewportSize({ width: 1400, height: 950 }); await page.goto(`${fixture.origin}/logbook`, { waitUntil: "domcontentloaded" }); await page.locator("#tab-logbook").waitFor({ state: "visible" }); await page.waitForTimeout(2500); // The log is there, newest first, with the band and mode of each contact. const opened = await readPanel(); assert.deepEqual(opened.rows.map((row) => row.call), ["DL3ABC", "SP9XYZ"]); assert.equal(opened.summary, "2 contacts"); // A phone contact shows the sideband it was worked on, not just "SSB". assert.ok(opened.rows[1].cells.includes("LSB"), `the row reads ${JSON.stringify(opened.rows[1].cells)}`); // The entry opened pre-filled: frequency and mode from the rig, the rig's // name, and the time from the server. assert.equal(opened.freq, "100", `the frequency pre-filled as "${opened.freq}"`); assert.equal(opened.mode, "USB", `the mode pre-filled as "${opened.mode}"`); assert.equal(opened.rig, "Primary fixture"); assert.match(opened.clock, /^\d{2}:\d{2}:\d{2}Z$/, `the clock reads "${opened.clock}"`); // ...and nothing else was: a report is the operator's to give. assert.equal(opened.call, ""); assert.equal(await page.evaluate(() => document.getElementById("log-rst-sent")?.value), ""); // Worked-before answers as the callsign is typed. await page.locator("#log-call").fill("DL3ABC"); await page.waitForTimeout(600); assert.match((await readPanel()).worked, /Worked before: 20m FT8/); await page.locator("#log-call").fill("OZ1NEW"); await page.waitForTimeout(600); assert.match((await readPanel()).worked, /Not worked before/); // Logging writes the contact and clears the entry for the next one. await page.locator("#log-rst-sent").fill("59"); await page.locator("#log-rst-rcvd").fill("57"); await page.locator("#log-save-btn").click(); await page.waitForTimeout(900); const afterLogging = await readPanel(); assert.deepEqual(afterLogging.rows.map((row) => row.call), ["OZ1NEW", "DL3ABC", "SP9XYZ"]); assert.equal(afterLogging.summary, "3 contacts"); assert.equal(afterLogging.call, "", "the entry kept the callsign after logging it"); assert.equal(afterLogging.freq, "100", "the entry did not re-open pre-filled"); // A decode row starts an entry rather than logging one: what was heard goes // into the form, and the operator finishes it. await page.evaluate(() => { window.navigateToTab("digital-modes"); }); await page.waitForTimeout(800); await page.locator('#tab-digital-modes .sub-tab[data-subtab="ft8"]').click(); await page.waitForTimeout(400); await page.evaluate(() => { window.trxPluginRuntime.dispatch("ft8", { message: "CQ SP2SJG JO94", snr_db: -7, dt_s: 0.2, freq_hz: 1200, ts_ms: Date.now(), rig_id: "rig-a", }); }); await page.waitForTimeout(600); await page.locator("#ft8-messages .ft8-log-btn").first().click(); await page.waitForTimeout(900); const fromDecode = await page.evaluate(() => ({ path: location.pathname, call: document.getElementById("log-call")?.value ?? "", grid: document.getElementById("log-grid")?.value ?? "", mode: document.getElementById("log-mode")?.value ?? "", rows: document.getElementById("log-rows")?.children.length ?? 0, })); assert.equal(fromDecode.path, "/logbook", `the Log button landed on ${fromDecode.path}`); assert.equal(fromDecode.call, "SP2SJG", "the heard callsign did not reach the entry"); assert.equal(fromDecode.grid, "JO94", "the heard locator did not reach the entry"); // The decoder answers for the mode, because a rig in DIG cannot. assert.equal(fromDecode.mode, "FT8", `the mode came through as "${fromDecode.mode}"`); assert.equal(fromDecode.rows, 3, "a decode logged itself instead of opening an entry"); // The ham layout is on offer, because this rig can transmit. const layouts = await page.evaluate(() => [...document.querySelectorAll("#operator-layout-select option")].map((option) => option.value)); assert.ok(layouts.includes("ham"), `the layouts are ${JSON.stringify(layouts)}`); // Choosing it the way the picker does. The