#!/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)"; // Les commentaires sont ignorés : un sélecteur cité dans une annotation // (ex. « .mobile-toolbar » dans un commentaire de style.css) ne doit pas // faire rétrograder ce helper sur le mauvais bloc mobile (BUG-103/CI #842). const source = css.replace(/\/\*[\s\S]*?\*\//g, ""); let idx = 0; while ((idx = source.indexOf(marker, idx)) !== -1) { const open = source.indexOf("{", idx); let depth = 0; let end = open; for (; end < source.length; end++) { if (source[end] === "{") depth++; else if (source[end] === "}") { depth--; if (depth === 0) break; } } const body = source.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."); }