|
|
@@ -9,6 +9,9 @@ import path from "node:path";
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { chromium } from "playwright-core";
|
|
|
|
import { chromium } from "playwright-core";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// page.evaluate callbacks run in the browser, not in this Node process.
|
|
|
|
|
|
|
|
/* global document */
|
|
|
|
|
|
|
|
|
|
|
|
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
|
const frontendDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
|
const webDir = path.resolve(frontendDir, "../assets/web");
|
|
|
|
const webDir = path.resolve(frontendDir, "../assets/web");
|
|
|
|
const generatedDir = path.join(webDir, "generated");
|
|
|
|
const generatedDir = path.join(webDir, "generated");
|
|
|
@@ -27,9 +30,28 @@ const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
|
|
|
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
|
|
|
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
|
|
|
const selectedRigs = [];
|
|
|
|
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([
|
|
|
|
const jsonRoutes = new Map([
|
|
|
|
["/auth/session", { authenticated: true, role: "control", auth_disabled: true }],
|
|
|
|
["/auth/session", { authenticated: true, role: "control", auth_disabled: true }],
|
|
|
|
["/decoders", []],
|
|
|
|
["/decoders", decoderRegistry],
|
|
|
|
["/rigs", rigsResponse],
|
|
|
|
["/rigs", rigsResponse],
|
|
|
|
["/status", {
|
|
|
|
["/status", {
|
|
|
|
info: {
|
|
|
|
info: {
|
|
|
@@ -195,6 +217,55 @@ try {
|
|
|
|
await page.locator("#tab-main").waitFor({ state: "visible" });
|
|
|
|
await page.locator("#tab-main").waitFor({ state: "visible" });
|
|
|
|
assert.equal(new URL(page.url()).pathname, "/");
|
|
|
|
assert.equal(new URL(page.url()).pathname, "/");
|
|
|
|
assert.deepEqual(runtimeErrors, []);
|
|
|
|
assert.deepEqual(runtimeErrors, []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Layout regressions -------------------------------------------------
|
|
|
|
|
|
|
|
// Every fault below shipped at some point while the rest of this file
|
|
|
|
|
|
|
|
// passed, because nothing here looked at geometry: a header whose height
|
|
|
|
|
|
|
|
// tracked the viewport, controls that stretched, a tab strip that ran under
|
|
|
|
|
|
|
|
// the controls, and a dropdown that opened underneath the spectrum.
|
|
|
|
|
|
|
|
for (const width of [1440, 1280, 1100, 900]) {
|
|
|
|
|
|
|
|
await page.setViewportSize({ width, height: 900 });
|
|
|
|
|
|
|
|
await page.waitForTimeout(250);
|
|
|
|
|
|
|
|
const header = await page.evaluate(() => {
|
|
|
|
|
|
|
|
const bar = document.querySelector(".tab-bar");
|
|
|
|
|
|
|
|
const nav = document.querySelector(".tab-bar-nav");
|
|
|
|
|
|
|
|
const actions = document.querySelector(".top-bar-actions");
|
|
|
|
|
|
|
|
const controls = [...actions.children]
|
|
|
|
|
|
|
|
.filter((el) => !el.hidden && el.getBoundingClientRect().height > 0)
|
|
|
|
|
|
|
|
.map((el) => Math.round(el.getBoundingClientRect().height));
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
barHeight: Math.round(bar.getBoundingClientRect().height),
|
|
|
|
|
|
|
|
// The tabs, not the strip: with the strip allowed to overflow its box
|
|
|
|
|
|
|
|
// shrinks while its content paints across the controls, so the
|
|
|
|
|
|
|
|
// container's own rect never registers the collision.
|
|
|
|
|
|
|
|
overlap: Math.round(Math.max(...[...nav.querySelectorAll(".tab")]
|
|
|
|
|
|
|
|
.filter((tab) => tab.offsetParent !== null)
|
|
|
|
|
|
|
|
.map((tab) => tab.getBoundingClientRect().right))
|
|
|
|
|
|
|
|
- actions.getBoundingClientRect().left),
|
|
|
|
|
|
|
|
heights: [...new Set(controls)],
|
|
|
|
|
|
|
|
pageScrollsSideways: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
assert.ok(header.barHeight <= 96, `header is ${header.barHeight}px at ${width}px; it should stay one row`);
|
|
|
|
|
|
|
|
assert.ok(header.overlap <= 0, `tab strip overlaps the controls by ${header.overlap}px at ${width}px`);
|
|
|
|
|
|
|
|
assert.ok(header.heights.length <= 2, `controls have heights ${header.heights.join(", ")} at ${width}px`);
|
|
|
|
|
|
|
|
assert.equal(header.pageScrollsSideways, false, `page scrolls sideways at ${width}px`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// A dropdown must paint over the page, not inside the header: fixed
|
|
|
|
|
|
|
|
// positioning escapes clipping but not the header's stacking context.
|
|
|
|
|
|
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
|
|
|
|
|
|
await page.waitForTimeout(250);
|
|
|
|
|
|
|
|
const menu = await page.evaluate(() => {
|
|
|
|
|
|
|
|
document.getElementById("mobile-more-btn").click();
|
|
|
|
|
|
|
|
const element = document.getElementById("mobile-more-menu");
|
|
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
|
|
|
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
|
|
|
|
|
|
return { width: Math.round(rect.width), height: Math.round(rect.height), onTop: element.contains(hit) };
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`);
|
|
|
|
|
|
|
|
assert.ok(menu.onTop, "menu is painted underneath the page");
|
|
|
|
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
await browser.close();
|
|
|
|
await browser.close();
|
|
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
|
|