Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b252717f6b | ||
|
|
92697b11c5 | ||
|
|
b409c57296 | ||
|
|
00191c8d7a | ||
|
|
56c363a517 | ||
|
|
5ad91b4ab6 | ||
|
|
1843522b45 | ||
|
|
41ddecf5a7 | ||
|
|
b06c37affa |
@@ -750,8 +750,9 @@ function elementById(id) {
|
|||||||
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
||||||
function anchorMenu(button, menu) {
|
function anchorMenu(button, menu) {
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
|
if (menu.parentElement !== document.body) document.body.appendChild(menu);
|
||||||
menu.style.position = "fixed";
|
menu.style.position = "fixed";
|
||||||
menu.style.zIndex = "200";
|
menu.style.zIndex = "9700";
|
||||||
menu.style.top = "0px";
|
menu.style.top = "0px";
|
||||||
menu.style.bottom = "auto";
|
menu.style.bottom = "auto";
|
||||||
menu.style.right = "auto";
|
menu.style.right = "auto";
|
||||||
@@ -798,7 +799,11 @@ function elementById(id) {
|
|||||||
if (open) anchorMenu(button, menu);
|
if (open) anchorMenu(button, menu);
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu();
|
if (!(event.target instanceof Node)) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") closeMenu();
|
if (event.key === "Escape") closeMenu();
|
||||||
@@ -838,7 +843,8 @@ function elementById(id) {
|
|||||||
more.id = "mobile-more-btn";
|
more.id = "mobile-more-btn";
|
||||||
more.className = "tab mobile-more-btn";
|
more.className = "tab mobile-more-btn";
|
||||||
more.type = "button";
|
more.type = "button";
|
||||||
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">More</span>';
|
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">Tools</span>';
|
||||||
|
more.setAttribute("aria-label", "Tools and settings");
|
||||||
more.setAttribute("aria-haspopup", "menu");
|
more.setAttribute("aria-haspopup", "menu");
|
||||||
more.setAttribute("aria-expanded", "false");
|
more.setAttribute("aria-expanded", "false");
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
@@ -5586,6 +5592,8 @@ function navigateToTab(name, options = {}) {
|
|||||||
_activeTab = name;
|
_activeTab = name;
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
|
const toolsBtn = document.getElementById("mobile-more-btn");
|
||||||
|
if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none");
|
||||||
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
|
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
|
||||||
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
||||||
const panel = document.getElementById(`tab-${name}`);
|
const panel = document.getElementById(`tab-${name}`);
|
||||||
|
|||||||
@@ -1051,6 +1051,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-statistics" class="tab-panel" style="display:none;">
|
<div id="tab-statistics" class="tab-panel" style="display:none;">
|
||||||
|
<h2 class="section-heading">Statistics</h2>
|
||||||
<template id="tmpl-statistics">
|
<template id="tmpl-statistics">
|
||||||
<div class="stats-controls">
|
<div class="stats-controls">
|
||||||
<div class="stats-control-group">
|
<div class="stats-control-group">
|
||||||
@@ -1191,6 +1192,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-settings" class="tab-panel" style="display:none;">
|
<div id="tab-settings" class="tab-panel" style="display:none;">
|
||||||
|
<h2 class="section-heading">Settings</h2>
|
||||||
<div class="sub-tab-bar">
|
<div class="sub-tab-bar">
|
||||||
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
|
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
|
||||||
<button class="sub-tab" data-subtab="settings-background-decode">Background Decode</button>
|
<button class="sub-tab" data-subtab="settings-background-decode">Background Decode</button>
|
||||||
@@ -1486,6 +1488,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-about" class="tab-panel" style="display:none;">
|
<div id="tab-about" class="tab-panel" style="display:none;">
|
||||||
|
<h2 class="section-heading">About</h2>
|
||||||
<div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div>
|
<div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div>
|
||||||
<template id="tmpl-about">
|
<template id="tmpl-about">
|
||||||
<div class="sub-tab-bar">
|
<div class="sub-tab-bar">
|
||||||
|
|||||||
@@ -1531,16 +1531,18 @@ small { color: var(--text-muted); }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* Never wraps and never scrolls: the occasional destinations live behind
|
/* Never wraps. It must still be able to shrink below its content, or it
|
||||||
* More, so four tabs plus More always fit. overflow-x here would clip the
|
* overlaps the controls; the menus it anchors are reparented to the body on
|
||||||
* dropdown this element anchors. */
|
* open, so clipping here no longer reaches them. */
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.tab-bar-nav .tab { flex: 0 0 auto; }
|
.tab-bar-nav .tab { flex: 0 0 auto; }
|
||||||
/* Icons before scrolling: every tab already carries one, and four icons plus
|
/* Icons before scrolling: every tab already carries one, and four icons plus
|
||||||
* More always fit, so the strip never has to hide a destination. */
|
* More always fit, so the strip never has to hide a destination. */
|
||||||
@media (max-width: 1180px) and (min-width: 701px) {
|
@media (max-width: 1360px) and (min-width: 701px) {
|
||||||
.tab-bar-nav .tab .tab-label { display: none; }
|
.tab-bar-nav .tab .tab-label { display: none; }
|
||||||
|
.tab-bar-nav .tab .tab-icon, .tab-bar-nav .tab .tab-more-icon { display: block; }
|
||||||
.tab-bar-nav .tab { padding: 0.5rem 0.6rem; }
|
.tab-bar-nav .tab { padding: 0.5rem 0.6rem; }
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
@@ -1564,6 +1566,7 @@ small { color: var(--text-muted); }
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.tab-label { display: block; }
|
.tab-label { display: block; }
|
||||||
|
.tab-more-icon { display: none; }
|
||||||
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
||||||
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
|
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
|
||||||
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
|
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
|
||||||
@@ -3277,7 +3280,7 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
|
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
|
||||||
}
|
}
|
||||||
.tab-icon { display: block; }
|
.tab-icon, .tab-more-icon { display: block; }
|
||||||
/* Shorten long tab labels to keep bottom nav compact */
|
/* Shorten long tab labels to keep bottom nav compact */
|
||||||
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
|
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
|
||||||
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
|
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
|
||||||
@@ -5185,7 +5188,6 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
.stats-controls {
|
.stats-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -5442,3 +5444,20 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
|||||||
.top-bar-more-menu.is-open { display: flex; }
|
.top-bar-more-menu.is-open { display: flex; }
|
||||||
.top-bar-more-menu > * { width: 100%; }
|
.top-bar-more-menu > * { width: 100%; }
|
||||||
.top-bar-more-menu select { width: 100%; max-width: none; }
|
.top-bar-more-menu select { width: 100%; max-width: none; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Page title for destinations reached through the Tools menu. The tab strip
|
||||||
|
* cannot show which of them is active, so each states its own name.
|
||||||
|
*
|
||||||
|
* The margin is the spacing on the plain block panels; #tab-statistics is a
|
||||||
|
* flex column with its own `gap`, where the margin lands on top of that gap
|
||||||
|
* and doubles the space under the title. */
|
||||||
|
.section-heading {
|
||||||
|
margin: 0 0 var(--space-3);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tab-statistics > .section-heading { margin-bottom: 0; }
|
||||||
|
|||||||
@@ -4600,6 +4600,12 @@ function navigateToTab(name: TabName, options: { updateHistory?: boolean; replac
|
|||||||
_activeTab = name;
|
_activeTab = name;
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
|
// A destination the strip hides is reached through Tools, so mark that
|
||||||
|
// button instead — otherwise the strip looks identical on all four of them.
|
||||||
|
// Derived from what is actually hidden rather than from a second copy of the
|
||||||
|
// grouping, which would drift from the one ui-core installs.
|
||||||
|
const toolsBtn = document.getElementById("mobile-more-btn");
|
||||||
|
if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none");
|
||||||
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
|
window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn);
|
||||||
document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => p.style.display = "none");
|
document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => p.style.display = "none");
|
||||||
const panel = document.getElementById(`tab-${name}`);
|
const panel = document.getElementById(`tab-${name}`);
|
||||||
|
|||||||
@@ -334,8 +334,14 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
// edge.
|
// edge.
|
||||||
function anchorMenu(button: HTMLElement, menu: HTMLElement) {
|
function anchorMenu(button: HTMLElement, menu: HTMLElement) {
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
|
// Reparent to the body. Fixed positioning escapes clipping but not
|
||||||
|
// stacking: the header sets `z-index: 2`, which makes it a stacking
|
||||||
|
// context, so any z-index the menu carries is confined below every panel
|
||||||
|
// that paints above level 2 — the spectrum overlays reach 9600. Only
|
||||||
|
// leaving that subtree lets the menu sit above the page.
|
||||||
|
if (menu.parentElement !== document.body) document.body.appendChild(menu);
|
||||||
menu.style.position = "fixed";
|
menu.style.position = "fixed";
|
||||||
menu.style.zIndex = "200";
|
menu.style.zIndex = "9700";
|
||||||
menu.style.top = "0px";
|
menu.style.top = "0px";
|
||||||
menu.style.bottom = "auto";
|
menu.style.bottom = "auto";
|
||||||
// The stylesheet pins these menus to the right of their anchor; leaving
|
// The stylesheet pins these menus to the right of their anchor; leaving
|
||||||
@@ -386,7 +392,8 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
if (open) anchorMenu(button, menu);
|
if (open) anchorMenu(button, menu);
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu();
|
if (!(event.target instanceof Node)) { closeMenu(); return; }
|
||||||
|
if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });
|
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });
|
||||||
|
|
||||||
@@ -441,7 +448,8 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
more.id = "mobile-more-btn";
|
more.id = "mobile-more-btn";
|
||||||
more.className = "tab mobile-more-btn";
|
more.className = "tab mobile-more-btn";
|
||||||
more.type = "button";
|
more.type = "button";
|
||||||
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">More</span>';
|
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">Tools</span>';
|
||||||
|
more.setAttribute("aria-label", "Tools and settings");
|
||||||
more.setAttribute("aria-haspopup", "menu");
|
more.setAttribute("aria-haspopup", "menu");
|
||||||
more.setAttribute("aria-expanded", "false");
|
more.setAttribute("aria-expanded", "false");
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
@@ -95,6 +95,18 @@ pub(crate) async fn about_index(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(&req, "text/html; charset=utf-8", c)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/statistics")]
|
||||||
|
pub(crate) async fn statistics_index(req: HttpRequest) -> impl Responder {
|
||||||
|
let c = gz_index_html();
|
||||||
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/bookmarks")]
|
||||||
|
pub(crate) async fn bookmarks_index(req: HttpRequest) -> impl Responder {
|
||||||
|
let c = gz_index_html();
|
||||||
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Favicon & logo
|
// Favicon & logo
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -637,6 +637,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::recorder_index)
|
.service(assets::recorder_index)
|
||||||
.service(assets::settings_index)
|
.service(assets::settings_index)
|
||||||
.service(assets::about_index)
|
.service(assets::about_index)
|
||||||
|
.service(assets::statistics_index)
|
||||||
|
.service(assets::bookmarks_index)
|
||||||
.service(assets::favicon)
|
.service(assets::favicon)
|
||||||
.service(assets::favicon_png)
|
.service(assets::favicon_png)
|
||||||
.service(assets::logo)
|
.service(assets::logo)
|
||||||
|
|||||||
Reference in New Issue
Block a user