#!/usr/bin/env node /** * ObsiGate — Toolbar action grouping tests (#90). * * Static non-regression checks on the source of the document action bar: * the buttons must be assembled in functional groups separated by spacers: * [TOC][pop-out][Bookmark] | [Editer][Source][.md][Forge] | [Copier][PDF][Export] | [Partager] * * Covers frontend/js/viewer.js (main app), frontend/popout.html (popout * window, kept aligned) and frontend/style.css (.action-sep rule). * * Usage: node tests/frontend/toolbar-order.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 viewer = readFileSync(path.join(ROOT, "frontend", "js", "viewer.js"), "utf8"); const popout = readFileSync(path.join(ROOT, "frontend", "popout.html"), "utf8"); const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8"); function test(label, fn) { try { fn(); console.log(" \u2713 " + label); } catch (err) { console.error(" \u2717 " + label + "\n " + err.message); process.exitCode = 1; } } // ── viewer.js : définition des groupes ─────────────────────────────────── test("viewer.js — group nav: TOC markdown-only, then pop-out, bookmark", () => { const m = viewer.match(/const navBtns = \[\];([\s\S]*?)const editBtns/); assert.ok(m, "navBtns block not found"); assert.match(m[1], /if \(data\.is_markdown\) navBtns\.push\(tocBtn\);/); assert.ok(m[1].includes("navBtns.push(openNewWindowBtn, bookmarkBtn, chatBtn);"), "navBtns group order"); }); test("viewer.js — group edit = [edit, source, .ext, forge?, pretty?] in order", () => { const m = viewer.match( /const editBtns = \[\];([\s\S]*?)const exportBtns/, ); assert.ok(m, "editBtns block not found"); const block = m[1]; const order = []; const re = /editBtns\.push\((\w+)\)/g; let g; while ((g = re.exec(block))) order.push(g[1]); // editBtn conditional, then sourceBtn, then ext-download conditional, then forge conditional assert.deepEqual(order, ["editBtn", "sourceBtn", "mdBtn", "forgeBtn", "prettyBtn"]); assert.ok(block.includes("if (!data.unsupported) editBtns.push(editBtn)")); assert.ok(block.includes("if (!data.is_image) editBtns.push(mdBtn)")); assert.ok(block.includes("if (data.is_markdown) editBtns.push(forgeBtn)")); // Pretty (text files) must land in the EDIT group, after the extension button assert.ok(block.indexOf("editBtns.push(prettyBtn)") > block.indexOf("editBtns.push(mdBtn)")); }); test("viewer.js — export group = [copier] + [pdf, export] markdown-only", () => { const m = viewer.match(/const exportBtns = \[([\s\S]*?)\];([\s\S]*?)const shareBtns/); assert.ok(m, "exportBtns block not found"); assert.ok(m[1].includes("copyBtn"), "copyBtn must be first in export group"); assert.match(m[2], /if \(data\.is_markdown\) exportBtns\.push\(pdfBtn,\s*exportWrap\)/, "PDF/Export must be markdown-only (text files: Copier alone)"); }); test("viewer.js — group share = [partager]", () => { assert.match(viewer, /const shareBtns = \[\s*shareBtn\s*\];/); }); test("viewer.js — assembly inserts an action-sep between non-empty groups", () => { const m = viewer.match( /\[navBtns,\s*editBtns,\s*exportBtns,\s*shareBtns\]\.forEach\(\(group\)\s*=>\s*\{([\s\S]*?)\}\);/, ); assert.ok(m, "group assembly loop not found"); const body = m[1]; assert.match(body, /if \(group\.length === 0\) return;/, "empty group must be skipped"); assert.match(body, /if \(fileActions\.length > 0\) fileActions\.push\(actionSep\(\)\);/, "spacer must be inserted between groups"); }); test("viewer.js — no legacy flat fileActions.push order remains", () => { assert.doesNotMatch( viewer, /const fileActions = \[\s*copyBtn,\s*sourceBtn\s*\];/, "old flat construction should be gone", ); }); // ── popout.html : alignement ─────────────────────────────────────────────── test("popout.html — grouped assembly matches viewer order", () => { assert.match( popout, /\[\[tocBtn,\s*bookmarkBtn\],\s*\[editBtn,\s*sourceBtn,\s*dlBtn\],\s*\[copyBtn,\s*pdfBtn\],\s*\[shareBtn\]\]/, ); assert.match(popout, /if \(actionsDiv\.childNodes\.length > 0\) actionsDiv\.appendChild\(actionSep\(\)\);/); }); test("popout.html — no leftover sequential appends remain", () => { assert.doesNotMatch(popout, /actionsDiv\.appendChild\((copyBtn|sourceBtn|dlBtn|pdfBtn|editBtn|tocBtn|shareBtn|bookmarkBtn)\);/, "buttons must be appended only via the grouped loop"); }); // ── style.css : règle du séparateur ────────────────────────────────────── test("style.css — .action-sep rule exists", () => { assert.match(css, /\.file-actions \.action-sep\s*\{[^}]*background:[^;]*;/); }); // ── #115 : barre d'outils collante au défilement ───────────────────────── test("viewer.js — action bar wrapped in a sticky .file-toolbar", () => { assert.match(viewer, /class:\s*"file-toolbar"[\s\S]{0,120}file-actions/, "the document action bar must live in a .file-toolbar wrapper"); }); test("popout.html — action bar wrapped in a .file-toolbar", () => { assert.match(popout, /className\s*=\s*"file-toolbar"/, "popout action bar must live in a .file-toolbar wrapper"); }); test("style.css — .file-toolbar is pinned to the top while scrolling", () => { const m = css.match(/\.file-toolbar\s*\{([\s\S]*?)\}/); assert.ok(m, ".file-toolbar rule missing"); assert.match(m[1], /position:\s*sticky/, ".file-toolbar must be sticky"); assert.match(m[1], /top:\s*0/, ".file-toolbar must pin to the top"); assert.match(m[1], /background:/, ".file-toolbar needs an opaque background"); }); test("style.css — reading mode hides the sticky toolbar", () => { assert.match(css, /body\.reading-mode\s+\.file-toolbar\s*[,{]/, "reading mode must hide .file-toolbar"); }); if (process.exitCode) { console.error("\nToolbar order tests FAILED"); } else { console.log("\nAll toolbar order tests passed."); }