[refactor](trx-frontend-http): extract the browser test fixture
browser-smoke.mjs carried its static server inline, which made it the only browser test that could exist: a second one would have had to copy 180 lines of routes to change a single capability flag. The server moves to tests/web-fixture.mjs behind startWebFixture(), with the rig's spectrum support, bookmarks and band plan as options. Serving a rig with a spectrum matters because that is where the layout actually lives — the panel, the strips above it and the waterfall are all gated on filter_controls, and the existing fixture reports a CAT-only rig, so none of it has ever been rendered under test. No change to what the smoke test checks. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3,191 +3,18 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import http from "node:http";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { chromium } from "playwright-core";
|
import { chromium } from "playwright-core";
|
||||||
|
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
||||||
|
|
||||||
// page.evaluate callbacks run in the browser, not in this Node process.
|
// page.evaluate callbacks run in the browser, not in this Node process.
|
||||||
/* global document, getComputedStyle */
|
/* global document, getComputedStyle */
|
||||||
|
|
||||||
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
const fixture = await startWebFixture();
|
||||||
const webDir = path.resolve(frontendDir, "../assets/web");
|
const { selectedRigs } = fixture;
|
||||||
const generatedDir = path.join(webDir, "generated");
|
const { browser, page, runtimeErrors } = await startBrowser(chromium);
|
||||||
const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
|
||||||
remote,
|
|
||||||
display_name: remote === "rig-a" ? "Primary fixture" : "Secondary fixture",
|
|
||||||
manufacturer: "Smoke",
|
|
||||||
model: "Fixture",
|
|
||||||
supported_modes: ["FM"],
|
|
||||||
tx: false,
|
|
||||||
filter_controls: false,
|
|
||||||
initialized: true,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
}));
|
|
||||||
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
|
||||||
const selectedRigs = [];
|
|
||||||
|
|
||||||
// A realistic decoder registry. Serving an empty one hid most of the
|
|
||||||
// application from this test: the decoder sub-tabs, their panels, the decode
|
|
||||||
// toggles and the bookmark decoder checkboxes are all built from it, so with
|
|
||||||
// no decoders only three of thirteen sub-tabs existed and none of the decoder
|
|
||||||
// UI was ever constructed.
|
|
||||||
const decoderRegistry = [
|
|
||||||
{ id: "ft8", label: "FT8", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "ft4", label: "FT4", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "ft2", label: "FT2", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "wspr", label: "WSPR", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "cw", label: "CW", activation: "toggle", active_modes: ["CW", "CWR"] },
|
|
||||||
{ id: "sat", label: "SAT", activation: "toggle", active_modes: ["FM"] },
|
|
||||||
{ id: "wefax", label: "WEFAX", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "ais", label: "AIS", activation: "toggle", active_modes: ["FM"] },
|
|
||||||
{ id: "aprs", label: "APRS", activation: "toggle", active_modes: ["FM"] },
|
|
||||||
{ id: "hf-aprs", label: "HF APRS", activation: "toggle", active_modes: ["USB"] },
|
|
||||||
{ id: "vdes", label: "VDES", activation: "toggle", active_modes: ["FM"] },
|
|
||||||
].map((decoder) => ({ ...decoder, background_decode: false, bookmark_selectable: true }));
|
|
||||||
|
|
||||||
const jsonRoutes = new Map([
|
|
||||||
["/auth/session", { authenticated: true, role: "control", auth_disabled: true }],
|
|
||||||
["/decoders", decoderRegistry],
|
|
||||||
["/rigs", rigsResponse],
|
|
||||||
["/status", {
|
|
||||||
info: {
|
|
||||||
manufacturer: "Smoke",
|
|
||||||
model: "Fixture",
|
|
||||||
revision: "1",
|
|
||||||
access: { Tcp: { addr: "127.0.0.1:0" } },
|
|
||||||
capabilities: {
|
|
||||||
min_freq_step_hz: 1,
|
|
||||||
supported_bands: [],
|
|
||||||
supported_modes: ["FM"],
|
|
||||||
num_vfos: 1,
|
|
||||||
lock: false,
|
|
||||||
lockable: false,
|
|
||||||
attenuator: false,
|
|
||||||
preamp: false,
|
|
||||||
rit: false,
|
|
||||||
rpt: false,
|
|
||||||
split: false,
|
|
||||||
tx: false,
|
|
||||||
tx_limit: false,
|
|
||||||
vfo_switch: false,
|
|
||||||
filter_controls: false,
|
|
||||||
signal_meter: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: { freq: { hz: 100_000_000 }, mode: "FM", tx_en: false, vfo: null, tx: null, rx: null, lock: null },
|
|
||||||
band: null,
|
|
||||||
enabled: true,
|
|
||||||
initialized: true,
|
|
||||||
cw_auto: false,
|
|
||||||
cw_wpm: 20,
|
|
||||||
cw_tone_hz: 700,
|
|
||||||
aprs_decode_enabled: false,
|
|
||||||
hf_aprs_decode_enabled: false,
|
|
||||||
cw_decode_enabled: false,
|
|
||||||
ft8_decode_enabled: false,
|
|
||||||
ft4_decode_enabled: false,
|
|
||||||
ft2_decode_enabled: false,
|
|
||||||
wspr_decode_enabled: false,
|
|
||||||
lrpt_decode_enabled: false,
|
|
||||||
wefax_decode_enabled: false,
|
|
||||||
recorder_enabled: false,
|
|
||||||
clients: 1,
|
|
||||||
rigctl_clients: 0,
|
|
||||||
audio_clients: 0,
|
|
||||||
active_remote: "rig-a",
|
|
||||||
remotes: ["rig-a", "rig-b"],
|
|
||||||
show_sdr_gain_control: false,
|
|
||||||
initial_map_zoom: 10,
|
|
||||||
spectrum_coverage_margin_hz: 50_000,
|
|
||||||
spectrum_usable_span_ratio: 0.92,
|
|
||||||
bandplan_enabled: false,
|
|
||||||
bandplan_region: "iaru1",
|
|
||||||
decode_history_retention_min: 1440,
|
|
||||||
server_connected: true,
|
|
||||||
}],
|
|
||||||
["/bandplan.json", {}],
|
|
||||||
["/api/recorder/status", []],
|
|
||||||
["/api/recorder/files", []],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const contentTypes = new Map([
|
|
||||||
[".css", "text/css; charset=utf-8"],
|
|
||||||
[".html", "text/html; charset=utf-8"],
|
|
||||||
[".js", "application/javascript; charset=utf-8"],
|
|
||||||
[".json", "application/json; charset=utf-8"],
|
|
||||||
[".png", "image/png"],
|
|
||||||
[".woff2", "font/woff2"],
|
|
||||||
]);
|
|
||||||
|
|
||||||
function assetPath(urlPath) {
|
|
||||||
// Every tab route has its own index handler on the server (see api/assets.rs),
|
|
||||||
// so a deep link or a refresh serves the SPA shell, not a 404.
|
|
||||||
if (!path.extname(urlPath)) return path.join(webDir, "index.html");
|
|
||||||
if (urlPath.startsWith("/vendor/")) return path.join(webDir, urlPath);
|
|
||||||
const generated = path.join(generatedDir, path.basename(urlPath));
|
|
||||||
if (urlPath.endsWith(".js")) return generated;
|
|
||||||
return path.join(webDir, urlPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = http.createServer(async (request, response) => {
|
|
||||||
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
||||||
if (url.pathname === "/select_rig" && request.method === "POST") {
|
|
||||||
const remote = url.searchParams.get("remote");
|
|
||||||
if (remote) {
|
|
||||||
rigsResponse.active_remote = remote;
|
|
||||||
jsonRoutes.get("/status").active_remote = remote;
|
|
||||||
selectedRigs.push(remote);
|
|
||||||
}
|
|
||||||
response.writeHead(200).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (jsonRoutes.has(url.pathname)) {
|
|
||||||
response.writeHead(200, { "content-type": "application/json" });
|
|
||||||
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (url.pathname === "/audio") {
|
|
||||||
response.writeHead(404).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (["/events", "/decode", "/spectrum", "/meter"].includes(url.pathname)) {
|
|
||||||
response.writeHead(200, {
|
|
||||||
"cache-control": "no-cache",
|
|
||||||
connection: "keep-alive",
|
|
||||||
"content-type": "text/event-stream",
|
|
||||||
});
|
|
||||||
response.write(": browser smoke stream\n\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const file = assetPath(url.pathname);
|
|
||||||
const bytes = await readFile(file);
|
|
||||||
response.writeHead(200, {
|
|
||||||
"content-type": contentTypes.get(path.extname(file)) ?? "application/octet-stream",
|
|
||||||
});
|
|
||||||
response.end(bytes);
|
|
||||||
} catch {
|
|
||||||
response.writeHead(404).end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
||||||
const address = server.address();
|
|
||||||
assert(address && typeof address === "object");
|
|
||||||
|
|
||||||
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
||||||
?? "/usr/bin/chromium";
|
|
||||||
const browser = await chromium.launch({ executablePath, headless: true, args: ["--no-sandbox"] });
|
|
||||||
const page = await browser.newPage();
|
|
||||||
const runtimeErrors = [];
|
|
||||||
page.on("pageerror", (error) => runtimeErrors.push(error.stack ?? error.message));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "domcontentloaded" });
|
await page.goto(`${fixture.origin}/`, { waitUntil: "domcontentloaded" });
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
assert.deepEqual(runtimeErrors, []);
|
assert.deepEqual(runtimeErrors, []);
|
||||||
await page.locator("#content").waitFor({ state: "visible" });
|
await page.locator("#content").waitFor({ state: "visible" });
|
||||||
@@ -281,7 +108,7 @@ try {
|
|||||||
// a test that asked whether the tab was displayed saw "none" for every tab
|
// a test that asked whether the tab was displayed saw "none" for every tab
|
||||||
// and lit Tools up on every refresh of every page.
|
// and lit Tools up on every refresh of every page.
|
||||||
for (const [route, tab, toolsLit] of [["/map", "map", false], ["/about", "about", true]]) {
|
for (const [route, tab, toolsLit] of [["/map", "map", false], ["/about", "about", true]]) {
|
||||||
await page.goto(`http://127.0.0.1:${address.port}${route}`, { waitUntil: "domcontentloaded" });
|
await page.goto(`${fixture.origin}${route}`, { waitUntil: "domcontentloaded" });
|
||||||
await page.locator(`#tab-${tab}`).waitFor({ state: "visible" });
|
await page.locator(`#tab-${tab}`).waitFor({ state: "visible" });
|
||||||
const marked = await page.evaluate(() => ({
|
const marked = await page.evaluate(() => ({
|
||||||
actives: [...document.querySelectorAll(".tab-bar .tab.active")].map((t) => t.dataset.tab || t.id),
|
actives: [...document.querySelectorAll(".tab-bar .tab.active")].map((t) => t.dataset.tab || t.id),
|
||||||
@@ -290,7 +117,7 @@ try {
|
|||||||
assert.ok(marked.actives.includes(tab), `${route} marks ${JSON.stringify(marked.actives)}`);
|
assert.ok(marked.actives.includes(tab), `${route} marks ${JSON.stringify(marked.actives)}`);
|
||||||
assert.equal(marked.tools, toolsLit, `${route}: Tools active is ${marked.tools}`);
|
assert.equal(marked.tools, toolsLit, `${route}: Tools active is ${marked.tools}`);
|
||||||
}
|
}
|
||||||
await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "domcontentloaded" });
|
await page.goto(`${fixture.origin}/`, { waitUntil: "domcontentloaded" });
|
||||||
await page.locator("#tab-main").waitFor({ state: "visible" });
|
await page.locator("#tab-main").waitFor({ state: "visible" });
|
||||||
assert.deepEqual(runtimeErrors, []);
|
assert.deepEqual(runtimeErrors, []);
|
||||||
|
|
||||||
@@ -344,5 +171,5 @@ try {
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
await fixture.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// The static server the browser tests load the real web assets from. It was
|
||||||
|
// inline in browser-smoke.mjs until a second browser test needed a rig with a
|
||||||
|
// spectrum: the interesting layout lives in the spectrum area, and none of it
|
||||||
|
// could be exercised while the only fixture served a CAT-only rig.
|
||||||
|
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||||
|
const webDir = path.resolve(frontendDir, "../assets/web");
|
||||||
|
const generatedDir = path.join(webDir, "generated");
|
||||||
|
|
||||||
|
// A realistic decoder registry. Serving an empty one hid most of the
|
||||||
|
// application from this test: the decoder sub-tabs, their panels, the decode
|
||||||
|
// toggles and the bookmark decoder checkboxes are all built from it, so with
|
||||||
|
// no decoders only three of thirteen sub-tabs existed and none of the decoder
|
||||||
|
// UI was ever constructed.
|
||||||
|
const DECODER_REGISTRY = [
|
||||||
|
{ id: "ft8", label: "FT8", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "ft4", label: "FT4", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "ft2", label: "FT2", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "wspr", label: "WSPR", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "cw", label: "CW", activation: "toggle", active_modes: ["CW", "CWR"] },
|
||||||
|
{ id: "sat", label: "SAT", activation: "toggle", active_modes: ["FM"] },
|
||||||
|
{ id: "wefax", label: "WEFAX", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "ais", label: "AIS", activation: "toggle", active_modes: ["FM"] },
|
||||||
|
{ id: "aprs", label: "APRS", activation: "toggle", active_modes: ["FM"] },
|
||||||
|
{ id: "hf-aprs", label: "HF APRS", activation: "toggle", active_modes: ["USB"] },
|
||||||
|
{ id: "vdes", label: "VDES", activation: "toggle", active_modes: ["FM"] },
|
||||||
|
].map((decoder) => ({ ...decoder, background_decode: false, bookmark_selectable: true }));
|
||||||
|
|
||||||
|
const CONTENT_TYPES = new Map([
|
||||||
|
[".css", "text/css; charset=utf-8"],
|
||||||
|
[".html", "text/html; charset=utf-8"],
|
||||||
|
[".js", "application/javascript; charset=utf-8"],
|
||||||
|
[".json", "application/json; charset=utf-8"],
|
||||||
|
[".png", "image/png"],
|
||||||
|
[".woff2", "font/woff2"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function assetPath(urlPath) {
|
||||||
|
// Every tab route has its own index handler on the server (see api/assets.rs),
|
||||||
|
// so a deep link or a refresh serves the SPA shell, not a 404.
|
||||||
|
if (!path.extname(urlPath)) return path.join(webDir, "index.html");
|
||||||
|
if (urlPath.startsWith("/vendor/")) return path.join(webDir, urlPath);
|
||||||
|
const generated = path.join(generatedDir, path.basename(urlPath));
|
||||||
|
if (urlPath.endsWith(".js")) return generated;
|
||||||
|
return path.join(webDir, urlPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the fixture server.
|
||||||
|
*
|
||||||
|
* `spectrum` turns the rig into an SDR: `filter_controls` is what gates the
|
||||||
|
* spectrum panel, and frames are pushed on the /spectrum stream from a centre
|
||||||
|
* frequency the test moves with `setCenterHz` to simulate tuning across bands.
|
||||||
|
*/
|
||||||
|
export async function startWebFixture({
|
||||||
|
spectrum = false,
|
||||||
|
bookmarks = [],
|
||||||
|
bandplan = {},
|
||||||
|
bandplanEnabled = false,
|
||||||
|
} = {}) {
|
||||||
|
const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
||||||
|
remote,
|
||||||
|
display_name: remote === "rig-a" ? "Primary fixture" : "Secondary fixture",
|
||||||
|
manufacturer: "Smoke",
|
||||||
|
model: "Fixture",
|
||||||
|
supported_modes: ["FM"],
|
||||||
|
tx: false,
|
||||||
|
filter_controls: spectrum,
|
||||||
|
initialized: true,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
}));
|
||||||
|
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
||||||
|
const selectedRigs = [];
|
||||||
|
const state = { centerHz: 7074000 };
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
info: {
|
||||||
|
manufacturer: "Smoke",
|
||||||
|
model: "Fixture",
|
||||||
|
revision: "1",
|
||||||
|
access: { Tcp: { addr: "127.0.0.1:0" } },
|
||||||
|
capabilities: {
|
||||||
|
min_freq_step_hz: 1,
|
||||||
|
supported_bands: [],
|
||||||
|
supported_modes: ["FM"],
|
||||||
|
num_vfos: 1,
|
||||||
|
lock: false,
|
||||||
|
lockable: false,
|
||||||
|
attenuator: false,
|
||||||
|
preamp: false,
|
||||||
|
rit: false,
|
||||||
|
rpt: false,
|
||||||
|
split: false,
|
||||||
|
tx: false,
|
||||||
|
tx_limit: false,
|
||||||
|
vfo_switch: false,
|
||||||
|
filter_controls: spectrum,
|
||||||
|
signal_meter: spectrum,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: { freq: { hz: 100_000_000 }, mode: "FM", tx_en: false, vfo: null, tx: null, rx: null, lock: null },
|
||||||
|
band: null,
|
||||||
|
enabled: true,
|
||||||
|
initialized: true,
|
||||||
|
cw_auto: false,
|
||||||
|
cw_wpm: 20,
|
||||||
|
cw_tone_hz: 700,
|
||||||
|
aprs_decode_enabled: false,
|
||||||
|
hf_aprs_decode_enabled: false,
|
||||||
|
cw_decode_enabled: false,
|
||||||
|
ft8_decode_enabled: false,
|
||||||
|
ft4_decode_enabled: false,
|
||||||
|
ft2_decode_enabled: false,
|
||||||
|
wspr_decode_enabled: false,
|
||||||
|
lrpt_decode_enabled: false,
|
||||||
|
wefax_decode_enabled: false,
|
||||||
|
recorder_enabled: false,
|
||||||
|
clients: 1,
|
||||||
|
rigctl_clients: 0,
|
||||||
|
audio_clients: 0,
|
||||||
|
active_remote: "rig-a",
|
||||||
|
remotes: ["rig-a", "rig-b"],
|
||||||
|
show_sdr_gain_control: false,
|
||||||
|
initial_map_zoom: 10,
|
||||||
|
spectrum_coverage_margin_hz: 50_000,
|
||||||
|
spectrum_usable_span_ratio: 0.92,
|
||||||
|
bandplan_enabled: bandplanEnabled,
|
||||||
|
bandplan_region: "iaru1",
|
||||||
|
decode_history_retention_min: 1440,
|
||||||
|
server_connected: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonRoutes = new Map([
|
||||||
|
["/auth/session", { authenticated: true, role: "control", auth_disabled: true }],
|
||||||
|
["/decoders", DECODER_REGISTRY],
|
||||||
|
["/rigs", rigsResponse],
|
||||||
|
["/status", status],
|
||||||
|
["/bookmarks", bookmarks],
|
||||||
|
["/bandplan.json", bandplan],
|
||||||
|
["/api/recorder/status", []],
|
||||||
|
["/api/recorder/files", []],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Flat i8 bins: the shape does not matter, only that frames arrive so the
|
||||||
|
// page has a spectrum range to place bookmarks and allocations against.
|
||||||
|
const spectrumBins = Buffer.alloc(512, 200);
|
||||||
|
const spectrumB64 = spectrumBins.toString("base64");
|
||||||
|
|
||||||
|
const server = http.createServer(async (request, response) => {
|
||||||
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
||||||
|
if (url.pathname === "/select_rig" && request.method === "POST") {
|
||||||
|
const remote = url.searchParams.get("remote");
|
||||||
|
if (remote) {
|
||||||
|
rigsResponse.active_remote = remote;
|
||||||
|
status.active_remote = remote;
|
||||||
|
selectedRigs.push(remote);
|
||||||
|
}
|
||||||
|
response.writeHead(200).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (jsonRoutes.has(url.pathname)) {
|
||||||
|
response.writeHead(200, { "content-type": "application/json" });
|
||||||
|
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.pathname === "/audio") {
|
||||||
|
response.writeHead(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (spectrum && url.pathname === "/spectrum") {
|
||||||
|
response.writeHead(200, {
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
connection: "keep-alive",
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
});
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
response.write(`event: b\ndata: ${state.centerHz},192000,${spectrumB64}\n\n`);
|
||||||
|
}, 100);
|
||||||
|
request.on("close", () => clearInterval(timer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (["/events", "/decode", "/spectrum", "/meter"].includes(url.pathname)) {
|
||||||
|
response.writeHead(200, {
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
connection: "keep-alive",
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
});
|
||||||
|
response.write(": browser smoke stream\n\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const file = assetPath(url.pathname);
|
||||||
|
const bytes = await readFile(file);
|
||||||
|
response.writeHead(200, {
|
||||||
|
"content-type": CONTENT_TYPES.get(path.extname(file)) ?? "application/octet-stream",
|
||||||
|
});
|
||||||
|
response.end(bytes);
|
||||||
|
} catch {
|
||||||
|
response.writeHead(404).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address !== "object") throw new Error("fixture server has no port");
|
||||||
|
|
||||||
|
return {
|
||||||
|
origin: `http://127.0.0.1:${address.port}`,
|
||||||
|
selectedRigs,
|
||||||
|
rigsResponse,
|
||||||
|
/** Moves the spectrum centre, i.e. tunes the fixture rig to another band. */
|
||||||
|
setCenterHz(hz) { state.centerHz = hz; },
|
||||||
|
close() {
|
||||||
|
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Launches headless Chromium and records any uncaught page error. */
|
||||||
|
export async function startBrowser(chromium) {
|
||||||
|
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||||
|
?? "/usr/bin/chromium";
|
||||||
|
const browser = await chromium.launch({ executablePath, headless: true, args: ["--no-sandbox"] });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const runtimeErrors = [];
|
||||||
|
page.on("pageerror", (error) => runtimeErrors.push(error.stack ?? error.message));
|
||||||
|
return { browser, page, runtimeErrors };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user