Pass predictions were a third view inside the Weather Satellite Decoder card, under Digital modes — a planning tool filed behind a decoder toggle, beside the FT8 and WEFAX panels it has nothing to do with. Nothing about knowing when a bird comes over belongs there. Move them to /satellites, reached from Tools alongside Statistics, Recorder, Settings and About: occasional destinations that live behind that menu rather than taking a slot in the operating strip. Adding a sixth strip button wrapped the phone nav onto two rows and cost the desktop strip its labels at 1280px, so the tab is hidden from the strip exactly the way its four peers already are — the nav is byte-for-byte what it was. The prediction code moves out of sat.ts into its own plugin that loads with the page, so the decoder card no longer carries it. Countdowns stop when the page is hidden and each visit reloads, since passes go stale while it is closed. The server grows a /satellites index route so a deep link or a refresh serves the SPA shell rather than a 404. Closes #47 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>
123 lines
5.5 KiB
JavaScript
123 lines
5.5 KiB
JavaScript
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
//
|
|
// 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();
|
|
}
|