#!/usr/bin/env node /** * ObsiGate — Configurations modal mobile usability non-regression tests (BUG-071). * * Static checks (no jsdom needed — runs in the "Frontend unit tests" CI step): * - BUG-071a: #config-nav shared the .help-nav rule that hides it below * 768px, but the config modal had no toggle (the help modal has * #help-hamburger) → the table of contents was unreachable on mobile. * The header must carry #config-hamburger (same .help-hamburger * treatment: hidden on desktop, visible on mobile) wired in config.js. * - BUG-071b: the TOC links were bare anchors with no JS — no active state, * no auto-collapse on mobile, unreliable scrolling inside the modal. * config.js must smooth-scroll to the section, mark it active and collapse * the nav on mobile, and reset the nav on open. * - BUG-071c: two-column grids (.ai-default-grid, .ai-provider-fields), * fixed-width add-rows (.config-add-row, 180/140/100px inline widths) and * single-line webhook/token/share items overflowed a 360px viewport. * style.css must stack/wrap them below 768px with 44px touch targets. * - BUG-071d: every #config-nav link target must exist (dead-anchor guard, * same class of bug as BUG-067 for the help modal). * - BUG-071e: data-i18n-attr supports several "attr:key" pairs (";"- * separated) so the toggle carries translated title AND aria-label. * * Usage: node tests/frontend/config-mobile.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 indexHtml = readFileSync(path.join(ROOT, "frontend", "index.html"), "utf8"); const configJs = readFileSync(path.join(ROOT, "frontend", "js", "config.js"), "utf8"); const i18nJs = readFileSync(path.join(ROOT, "frontend", "js", "i18n.js"), "utf8"); const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8"); const fr = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8")); const en = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "utf8")); function test(label, fn) { try { fn(); console.log(" ✓ " + label); } catch (err) { console.error(" ✗ " + label + "\n " + err.message); process.exitCode = 1; } } // ── BUG-071a: header TOC toggle ───────────────────────────────────────────── test("index.html — #config-hamburger exists in the config modal header", () => { const modal = indexHtml.match(/
([\s\S]*?)
/); assert.ok(modal, "#config-modal with #config-body not found"); const header = modal[1].match(/
([\s\S]*?)<\/div>\s*<\/div>/); assert.ok(header, "config modal .editor-header not found"); assert.match(header[1], /id="config-hamburger"/, "no #config-hamburger in the config header — TOC unreachable on mobile"); assert.match(header[1], /help-hamburger/, "the toggle must reuse .help-hamburger (desktop-hidden, mobile-visible)"); assert.match(header[1], /aria-label/, "the toggle needs an accessible label"); assert.match(header[1], /config\.toc_toggle/, "the toggle label must use the i18n key config.toc_toggle"); }); test("i18n — config.toc_toggle exists in FR and EN", () => { assert.ok(fr["config.toc_toggle"], "fr.json missing config.toc_toggle"); assert.ok(en["config.toc_toggle"], "en.json missing config.toc_toggle"); assert.notEqual(fr["config.toc_toggle"], "config.toc_toggle", "FR value must be translated"); assert.notEqual(en["config.toc_toggle"], "config.toc_toggle", "EN value must be translated"); }); // ── BUG-071b: TOC behaviour in config.js ──────────────────────────────────── test("config.js — hamburger toggles #config-nav", () => { assert.match(configJs, /getElementById\("config-hamburger"\)/, "no binding on #config-hamburger"); assert.match(configJs, /_setConfigNav\(/, "TOC open/close helper missing"); }); test("config.js — TOC links smooth-scroll, mark active, collapse on mobile", () => { assert.match(configJs, /#config-nav[\s\S]{0,400}?help-nav-link/, "no handler on the #config-nav links"); assert.match(configJs, /scrollIntoView/, "section scroll must use scrollIntoView inside the modal"); assert.match(configJs, /innerWidth <= 768/, "the nav must auto-collapse on mobile viewports only"); }); test("config.js — TOC display reset when the modal opens", () => { assert.match(configJs, /configNavOnOpen[\s\S]{0,120}?style\.display = ''/, "stale inline display would stick across sessions"); }); // ── BUG-071c: mobile CSS ──────────────────────────────────────────────────── test("style.css — config TOC becomes a capped top block on mobile", () => { assert.match(css, /#config-modal #config-nav/, "no mobile rule scoped to #config-modal #config-nav"); assert.match(css, /#config-modal #config-nav[\s\S]{0,400}?max-height/, "the opened TOC must be height-capped so content stays reachable"); }); test("style.css — two-column config grids stack on mobile", () => { assert.match(css, /#config-modal \.ai-default-grid/, ".ai-default-grid still 2 columns on mobile"); assert.match(css, /#config-modal \.ai-provider-fields/, ".ai-provider-fields still 3fr/2fr on mobile"); assert.match(css, /grid-template-columns: 1fr;/, "mobile grids must collapse to a single column"); }); test("style.css — add-rows wrap and fixed inline widths are neutralised", () => { assert.match(css, /#config-modal \.config-add-row/, "no mobile rule for .config-add-row (token/webhook rows overflow)"); assert.match(css, /width: auto !important/, "fixed inline widths (180/140/100px) must be overridden on mobile"); assert.match(css, /min-height: 44px/, "mobile action controls need 44px touch targets"); }); test("style.css — webhook/token/share rows wrap on mobile", () => { for (const cls of ["webhook-item", "token-item", "share-item"]) { assert.match(css, new RegExp("#config-modal \\." + cls), `.${cls} has no mobile wrap rule`); } }); // ── BUG-071d: dead-anchor guard ───────────────────────────────────────────── test("index.html — every #config-nav link resolves to an element id", () => { const nav = indexHtml.match(/