feat(viewer): #90 regroupement fonctionnel des boutons de la barre d'actions + spacers
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:
@@ -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.");
|
||||
}
|
||||
Reference in New Issue
Block a user