// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later // Pass predictions used to be a third view inside the weather-satellite // decoder, under Digital modes — a planning tool filed with the decoders it has // nothing to do with. It is its own page now, so what this guards is that the // page exists at its own address, renders passes, and that the decoder card no // longer offers the view it gave up. import assert from "node:assert/strict"; import { chromium } from "playwright-core"; import { startBrowser, startWebFixture } from "./web-fixture.mjs"; /* global document, window, getComputedStyle */ const HOUR = 3_600_000; const now = Date.now(); const satPasses = { satellite_count: 2, passes: [ // In progress right now, so the page has a countdown to run. { satellite: "NOAA 19", category: "weather", aos_ms: now - 4 * 60_000, los_ms: now + 6 * 60_000, max_elevation_deg: 62.4, duration_s: 600, azimuth_aos_deg: 10, azimuth_los_deg: 190, }, { satellite: "ISS", category: "amateur", aos_ms: now + 2 * HOUR, los_ms: now + 2 * HOUR + 480_000, max_elevation_deg: 18.2, duration_s: 480, azimuth_aos_deg: 200, azimuth_los_deg: 20, }, ], }; const fixture = await startWebFixture({ satPasses }); const { browser, page, runtimeErrors } = await startBrowser(chromium); try { await page.setViewportSize({ width: 1500, height: 950 }); // The page is reachable at its own address, not through a decoder sub-view. // #content is the radio panel on the main tab, so the readiness signal for a // deep link is the destination panel itself. await page.goto(`${fixture.origin}/satellites`, { waitUntil: "domcontentloaded" }); await page.locator("#tab-satellites").waitFor({ state: "visible" }); await page.waitForFunction( () => (document.getElementById("sat-pred-list")?.childElementCount ?? 0) > 0, null, { timeout: 5000 }, ); const rendered = await page.evaluate(() => ({ current: document.getElementById("sat-pred-current-list").textContent, upcoming: document.getElementById("sat-pred-list").textContent, status: document.getElementById("sat-pred-status").textContent, activeTab: document.querySelector(".tab-bar .tab.active")?.dataset.tab, })); assert.match(rendered.current, /NOAA 19/, `current passes read "${rendered.current}"`); assert.match(rendered.upcoming, /ISS/, `upcoming passes read "${rendered.upcoming}"`); assert.match(rendered.status, /1 active/, `status reads "${rendered.status}"`); assert.equal(rendered.activeTab, "satellites", "the Satellites tab is the active one"); // A pass in progress counts down, so the seconds have to move on their own. const firstTick = await page.evaluate( () => document.querySelector(".sat-pred-col-countdown[data-los]")?.textContent); await page.waitForTimeout(1600); const secondTick = await page.evaluate( () => document.querySelector(".sat-pred-col-countdown[data-los]")?.textContent); assert.notEqual(firstTick, secondTick, `the countdown sat at ${firstTick}`); // Leaving stops the countdown rather than leaving a timer running behind a // hidden page. await page.evaluate(() => { window.navigateToTab("main"); }); await page.waitForTimeout(300); const afterLeaving = await page.evaluate(() => ({ visible: document.getElementById("tab-satellites").style.display, rows: document.getElementById("sat-pred-current-list").childElementCount, })); assert.equal(afterLeaving.visible, "none", "the page stayed on screen after navigating away"); assert.equal(afterLeaving.rows, 0, "the countdown rows outlived the page"); // Coming back reloads rather than showing whatever was there before. await page.evaluate(() => { window.navigateToTab("satellites"); }); await page.waitForFunction( () => (document.getElementById("sat-pred-list")?.childElementCount ?? 0) > 0, null, { timeout: 5000 }, ); // The page is an occasional destination, so it lives behind Tools rather than // taking a slot in the operating strip — the same treatment Statistics, // Recorder, Settings and About get. const nav = await page.evaluate(() => ({ inStrip: getComputedStyle(document.querySelector('.tab-bar-nav .tab[data-tab="satellites"]')).display, inTools: !!document.querySelector('#mobile-more-menu [data-navigate-tab="satellites"]'), })); assert.equal(nav.inStrip, "none", "Satellites took a slot in the operating strip"); assert.ok(nav.inTools, "Satellites is not reachable from Tools"); // The decoder card keeps Live and History, and no longer offers Predictions. await page.goto(`${fixture.origin}/digital-modes`, { waitUntil: "domcontentloaded" }); await page.locator("#tab-digital-modes").waitFor({ state: "visible" }); const satCard = await page.evaluate(() => ({ predictionsButton: !!document.getElementById("sat-view-predictions"), predictionsView: !!document.getElementById("sat-predictions-view"), live: !!document.getElementById("sat-view-live"), history: !!document.getElementById("sat-view-history"), })); assert.equal(satCard.predictionsButton, false, "the decoder card still offers Predictions"); assert.equal(satCard.predictionsView, false, "the old predictions view is still in the page"); assert.ok(satCard.live && satCard.history, "the decoder card lost Live/History"); assert.deepEqual(runtimeErrors, [], "the page threw while showing predictions"); console.log("satellite predictions page tests passed"); } finally { await browser.close(); await fixture.close(); }