feat(viewer): #90 regroupement fonctionnel des boutons de la barre d'actions + spacers
CI / lint (push) Successful in 1m18s
CI / security (push) Successful in 55s
CI / test (push) Successful in 2m10s
CI / build (push) Successful in 48s
CI / e2e (push) Successful in 11m0s

Ordre: [TOC][pop-out][Bookmark] | [Editer][Source][.md][Forge] | [Copier][PDF][Export] | [Partager]
- viewer.js: construction par groupes nav/edit/export/share, spacer .action-sep
  entre groupes non vides; Pretty ancre au groupe edition
- popout.html: meme assemblage groupe, ordre aligne
- style.css: regle .file-actions .action-sep (1px, var(--border-md))
- tests: tests/frontend/toolbar-order.test.mjs (9) branche au CI
This commit is contained in:
2026-09-14 22:47:46 -04:00
parent 0201d963b7
commit 14b99826eb
8 changed files with 192 additions and 17 deletions
+115
View File
@@ -0,0 +1,115 @@
#!/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, pop-out, bookmark]", () => {
assert.match(
viewer,
/const navBtns = \[\s*tocBtn,\s*openNewWindowBtn,\s*bookmarkBtn\s*\];/,
);
});
test("viewer.js — group edit = [edit, source, .md, forge] (pretty appended)", () => {
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 mdBtn 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)"));
});
test("viewer.js — group export = [copier, pdf, export]", () => {
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], /exportBtns\.push\(pdfBtn,\s*exportWrap\)/);
});
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:[^;]*;/);
});
if (process.exitCode) {
console.error("\nToolbar order tests FAILED");
} else {
console.log("\nAll toolbar order tests passed.");
}