133 lines
5.2 KiB
JavaScript
133 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Mobile bottom toolbar clearance non-regression tests (BUG-073).
|
|
*
|
|
* Static checks (no jsdom — runs in the "Frontend unit tests" CI step):
|
|
* - BUG-073: the fixed bottom toolbar (#mobile-toolbar, 64px + safe-area)
|
|
* covered the last pixels of every document/page because the clearance
|
|
* rule in the ≤768px media block targeted `.main-layout` — a class that
|
|
* does not exist in index.html (the real layout container is
|
|
* `.main-body`). The dead selector reserved no space, so #content-area
|
|
* extended behind the bar and the end of the content could never be
|
|
* scrolled clear of it.
|
|
* - The clearance must live on `.main-body` inside the mobile media block
|
|
* (desktop keeps no padding), the dead `.main-layout` rules must be gone,
|
|
* reading mode (toolbar hidden) must reset the padding, and the
|
|
* now-playing rule must not double-count the toolbar zone.
|
|
*
|
|
* Usage: node tests/frontend/mobile-toolbar.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
|
|
const indexHtml = readFileSync(path.join(ROOT, "frontend", "index.html"), "utf8");
|
|
|
|
function test(label, fn) {
|
|
try {
|
|
fn();
|
|
console.log(" \u2713 " + label);
|
|
} catch (err) {
|
|
console.error(" \u2717 " + label + "\n " + err.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
/** Body of the first @media (max-width: 768px) block whose body contains `needle`. */
|
|
function mobileBlockContaining(needle) {
|
|
const marker = "@media (max-width: 768px)";
|
|
let idx = 0;
|
|
while ((idx = css.indexOf(marker, idx)) !== -1) {
|
|
const open = css.indexOf("{", idx);
|
|
let depth = 0;
|
|
let end = open;
|
|
for (; end < css.length; end++) {
|
|
if (css[end] === "{") depth++;
|
|
else if (css[end] === "}") {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
const body = css.slice(open + 1, end);
|
|
if (body.includes(needle)) return body;
|
|
idx = end + 1;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── The layout container the clearance must target exists in the DOM ───────
|
|
test("index.html — layout container .main-body and #mobile-toolbar both exist", () => {
|
|
assert.match(indexHtml, /class="main-body"/, "index.html must define the .main-body layout container");
|
|
assert.match(indexHtml, /class="mobile-toolbar"/, "index.html must define the .mobile-toolbar bar");
|
|
});
|
|
|
|
// ── BUG-073: clearance inside the mobile media block ────────────────────────
|
|
const toolbarBlock = mobileBlockContaining(".mobile-toolbar");
|
|
|
|
test("style.css — a ≤768px media block styles .mobile-toolbar", () => {
|
|
assert.ok(toolbarBlock, "no @media (max-width: 768px) block contains .mobile-toolbar");
|
|
});
|
|
|
|
test("style.css — mobile clearance targets .main-body (64px + safe-area)", () => {
|
|
assert.ok(toolbarBlock, "toolbar media block not found");
|
|
assert.match(
|
|
toolbarBlock,
|
|
/\.main-body \{[^}]*padding-bottom:[^}]*calc\(64px \+ env\(safe-area-inset-bottom/,
|
|
"the toolbar clearance must pad .main-body inside the mobile block (BUG-073: .main-layout does not exist in index.html)",
|
|
);
|
|
});
|
|
|
|
test("style.css — dead .main-layout clearance rules are gone", () => {
|
|
assert.ok(toolbarBlock, "toolbar media block not found");
|
|
assert.doesNotMatch(
|
|
toolbarBlock,
|
|
/\.main-layout \{[^}]*padding-bottom/,
|
|
".main-layout clearance is a dead selector in index.html (BUG-073)",
|
|
);
|
|
assert.doesNotMatch(
|
|
toolbarBlock,
|
|
/\.editor-modal\.active ~ \.main-layout/,
|
|
"the sibling clearance reset targeted a selector that never matches (BUG-073)",
|
|
);
|
|
});
|
|
|
|
test("style.css — desktop .main-body carries no toolbar padding", () => {
|
|
const base = css.match(/\.main-body \{[^}]*\}/);
|
|
assert.ok(base, ".main-body base rule not found");
|
|
assert.doesNotMatch(base[0], /padding-bottom/, "toolbar clearance must stay mobile-only");
|
|
});
|
|
|
|
// ── Companion rules ─────────────────────────────────────────────────────────
|
|
test("style.css — reading mode resets the clearance (toolbar hidden)", () => {
|
|
assert.match(
|
|
css,
|
|
/body\.reading-mode \.main-body \{[^}]*padding-bottom: 0/,
|
|
"reading mode hides #mobile-toolbar: .main-body must release the padding",
|
|
);
|
|
});
|
|
|
|
test("style.css — np-active keeps only the dock zone (no toolbar double-count)", () => {
|
|
assert.match(
|
|
css,
|
|
/body\.np-active \.content-area \{[^}]*padding-bottom: 76px/,
|
|
"toolbar clearance now comes from .main-body: np-active must pad the dock zone only (76px)",
|
|
);
|
|
assert.doesNotMatch(
|
|
css,
|
|
/body\.np-active \.content-area \{[^}]*calc\(64px \+ env\(safe-area-inset-bottom[^}]*\+ 76px/,
|
|
"the old formula double-counted the toolbar zone already cleared by .main-body",
|
|
);
|
|
});
|
|
|
|
if (process.exitCode) {
|
|
console.error("\nMobile toolbar tests FAILED");
|
|
} else {
|
|
console.log("\nAll mobile toolbar tests passed.");
|
|
}
|