` +
+ `
` +
`>${escapeAprsHtml(packet.destCall || "--")}` +
`${escapeAprsHtml(packet.path || "no path")}` +
`${escapeAprsHtml(aprsAgeText(packet._tsMs))}` +
@@ -355,9 +355,9 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions
+ `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}`
: "") +
`
` +
- `
${renderAprsInfo(packet)}
` +
+ `
${renderAprsInfo(packet)}
` +
(packet.info_bytes?.length
- ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
`
+ ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
`
: "") +
`
` +
(hasPosition ? `` : "") +
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
index ee21064b..f021d128 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
@@ -178,7 +178,20 @@ const APRS_FRAMES = [
...frame,
}));
-const aprsFixture = await startWebFixture({ spectrum: true, history: { aprs: APRS_FRAMES } });
+// The AIS list is the same shape: a line per message, saying where the vessel
+// is and what it is doing, with the identifiers behind it.
+const AIS_MESSAGES = [
+ { message_type: 1, mmsi: 244660001, vessel_name: "NEDERLAND", channel: "A",
+ lat: 54.35, lon: 18.65, sog_knots: 8.2, cog_deg: 91.4 },
+ { message_type: 5, mmsi: 244660002, vessel_name: "STENA SPIRIT", channel: "B",
+ callsign: "PBTX", destination: "GDANSK" },
+].map((message, index) => ({ rig_id: "rig-a", ts_ms: Date.now() - index * 1000, ...message }));
+
+const aprsFixture = await startWebFixture({
+ spectrum: true,
+ mode: "AIS",
+ history: { aprs: APRS_FRAMES, ais: AIS_MESSAGES },
+});
const aprs = await startBrowser(chromium);
try {
@@ -194,7 +207,7 @@ try {
tag: row.tagName,
height: Math.round(row.getBoundingClientRect().height),
type: row.querySelector(".aprs-badge-type")?.textContent?.trim() ?? "",
- summary: row.querySelector(".aprs-line-summary")?.textContent?.trim() ?? "",
+ summary: row.querySelector(".decode-line-summary")?.textContent?.trim() ?? "",
})));
// Newest first, so find each by the type it carries rather than by position.
const summaryOf = (type) => rows.find((row) => row.type === type)?.summary ?? "";
@@ -213,20 +226,49 @@ try {
// The frame as it arrived is still there, one click away.
await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" })
- .locator(".aprs-line").first().click();
+ .locator(".decode-line").first().click();
await aprs.page.waitForTimeout(200);
const expanded = await aprs.page.evaluate(() => {
const row = document.querySelector("#aprs-packets .aprs-packet[open]");
return {
open: row.hasAttribute("open"),
- raw: row.querySelector(".aprs-expanded-raw")?.textContent?.trim() ?? "",
- meta: row.querySelector(".aprs-expanded-meta")?.textContent ?? "",
+ raw: row.querySelector(".decode-expanded-raw")?.textContent?.trim() ?? "",
+ meta: row.querySelector(".decode-expanded-meta")?.textContent ?? "",
};
});
assert.equal(expanded.open, true, "the frame did not open");
assert.match(expanded.raw, /^_10090556c220s004g005t077/, `raw frame: ${expanded.raw}`);
assert.match(expanded.meta, /WIDE1-1/, `expanded meta: ${expanded.meta}`);
+ // AIS, on the same row.
+ await aprs.page.locator('.sub-tab[data-subtab="ais"]').click();
+ await aprs.page.waitForTimeout(300);
+ const aisRows = await aprs.page.evaluate(() =>
+ [...document.querySelectorAll("#ais-messages .ais-message")].map((row) => ({
+ tag: row.tagName,
+ height: Math.round(row.getBoundingClientRect().height),
+ name: row.querySelector(".ais-call")?.textContent?.trim() ?? "",
+ summary: row.querySelector(".decode-line-summary")?.textContent?.trim() ?? "",
+ })));
+ assert.equal(aisRows.length, AIS_MESSAGES.length, `rendered ${aisRows.length} messages`);
+ for (const row of aisRows) {
+ assert.equal(row.tag, "DETAILS", "an AIS message is not expandable in place");
+ assert.ok(row.height < 44, `an AIS message is ${row.height}px tall`);
+ }
+ const positionRow = aisRows.find((row) => row.name === "NEDERLAND");
+ const staticRow = aisRows.find((row) => row.name === "STENA SPIRIT");
+ assert.match(positionRow?.summary ?? "", /54\.3500, 18\.6500/, `position: ${positionRow?.summary}`);
+ assert.match(positionRow?.summary ?? "", /8\.2 kn/, `position: ${positionRow?.summary}`);
+ // A static report carries no fix, so it says where the vessel is going.
+ assert.match(staticRow?.summary ?? "", /PBTX -> GDANSK/, `static: ${staticRow?.summary}`);
+
+ await aprs.page.locator("#ais-messages .ais-message .decode-line").first().click();
+ await aprs.page.waitForTimeout(200);
+ const aisExpanded = await aprs.page.evaluate(() =>
+ document.querySelector("#ais-messages .ais-message[open] .decode-expanded-meta")?.textContent ?? "");
+ assert.match(aisExpanded, /MMSI 2446600/, `expanded AIS: ${aisExpanded}`);
+ assert.match(aisExpanded, /MHz/, `expanded AIS: ${aisExpanded}`);
+
assert.deepEqual(aprs.runtimeErrors, []);
} finally {
await aprs.browser.close();