Files
ObsiGate/tests/frontend/pane-manager.test.mjs
T
bruno 050c3d2515 test(pane-manager): tests JSDOM pour #75 I2
- tests/frontend/pane-manager.test.mjs : 9 tests d'intégration frontend via JSDOM 30
  Couvre factory, init, open/activate/close, isolation entre panes, drag/drop smoke
- tests/frontend/package.json + package-lock.json : jsdom en devDependency
- .gitea/workflows/ci.yml : nouvelle étape 'Frontend JSDOM tests (PaneManager)'
  qui installe jsdom au besoin puis lance pane-manager.test.mjs
2026-09-07 08:54:41 -04:00

272 lines
9.5 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate — JSDOM integration tests for PaneManager (ROADMAP #75 I2).
*
* Loads the actual pane-manager.js ES module in a JSDOM environment and
* verifies DOM-level behavior: tab creation via open(), activation,
* removal via close(), pane isolation, drag/drop smoke.
*
* Usage: node tests/frontend/pane-manager.test.mjs
*/
import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import { fileURLToPath, pathToFileURL } from "node:url";
import path from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPO_ROOT = path.resolve(__dirname, "..", "..");
// ── JSDOM bootstrap ─────────────────────────────────────────────────────────
const dom = new JSDOM(
`<!DOCTYPE html>
<html>
<body>
<div id="content-wrapper">
<div id="dashboard-home"></div>
<div id="tab-bar" class="tab-bar">
<div id="tab-list" class="tab-list"></div>
</div>
<div id="content-area"></div>
</div>
</body>
</html>`,
{ url: "http://localhost/", pretendToBeVisual: true }
);
const w = dom.window;
globalThis.window = w;
globalThis.document = w.document;
globalThis.HTMLElement = w.HTMLElement;
globalThis.Element = w.Element;
globalThis.Node = w.Node;
globalThis.Event = w.Event;
globalThis.CustomEvent = w.CustomEvent;
globalThis.DragEvent = w.DragEvent || w.Event;
globalThis.MouseEvent = w.MouseEvent || w.Event;
globalThis.localStorage = w.localStorage;
globalThis.sessionStorage = w.sessionStorage;
Object.defineProperty(globalThis, "navigator", {
value: w.navigator,
configurable: true,
writable: true,
});
// ── Helpers ─────────────────────────────────────────────────────────────────
function $$(sel) {
return Array.from(document.querySelectorAll(sel));
}
function resetBody(html) {
document.body.innerHTML = html;
}
// ── Dynamic import (after globals are set) ──────────────────────────────────
const pm = await import(
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "pane-manager.js")).href
);
const { createPaneTabManager, PaneManager, getActiveTabManager, getActiveContentArea } = pm;
let testCount = 0;
let passCount = 0;
async function test(name, fn) {
testCount++;
try {
await fn();
console.log(` ✓ ${name}`);
passCount++;
} catch (e) {
console.log(` ✗ ${name}`);
console.log(` ${e.message}`);
if (e.stack) console.log(` ${e.stack.split("\n").slice(1, 3).join("\n ")}`);
}
}
// ── Test suite ──────────────────────────────────────────────────────────────
console.log("\n── pane-manager.js JSDOM integration tests (#75 I2) ──\n");
await test("createPaneTabManager returns expected shape", () => {
resetBody(`
<div id="pane-t1"><div class="tab-bar"></div><div id="tl1" class="tab-list"></div><div class="content-area"></div></div>
`);
const tm = createPaneTabManager("t1");
assert.equal(tm.paneId, "t1");
assert.equal(typeof tm.open, "function");
assert.equal(typeof tm.activate, "function");
assert.equal(typeof tm.close, "function");
assert.equal(typeof tm.init, "function");
});
await test("init() binds tab bar / list / content area to provided DOM", () => {
resetBody(`
<div id="pane-t2">
<div class="tab-bar"></div>
<div id="tb2" class="tab-list"></div>
<div id="ca2" class="content-area"></div>
</div>
`);
const tm = createPaneTabManager("t2");
const bar = document.querySelector("#pane-t2 .tab-bar");
const list = document.querySelector("#tb2");
const area = document.querySelector("#ca2");
tm.init(bar, list, area);
assert.equal(tm.getTabBar(), bar);
assert.equal(tm.getTabList(), list);
assert.equal(tm.getContentArea(), area);
});
await test("open() appends a tab-item to the tab-list", async () => {
resetBody(`
<div id="pane-t3">
<div class="tab-bar"></div>
<div id="tl3" class="tab-list"></div>
<div class="content-area"></div>
</div>
`);
const tm = createPaneTabManager("t3");
tm.init(
document.querySelector("#pane-t3 .tab-bar"),
document.querySelector("#tl3"),
document.querySelector("#pane-t3 .content-area")
);
await tm.open("vault1", "/notes/foo.md");
const items = $$("#tl3 .tab-item");
assert.equal(items.length, 1, `expected 1 tab-item, got ${items.length}`);
assert.match(items[0].textContent, /foo/);
});
await test("activate() switches .active class between tabs", async () => {
resetBody(`
<div id="pane-t4">
<div class="tab-bar"></div>
<div id="tl4" class="tab-list"></div>
<div class="content-area"></div>
</div>
`);
const tm = createPaneTabManager("t4");
tm.init(
document.querySelector("#pane-t4 .tab-bar"),
document.querySelector("#tl4"),
document.querySelector("#pane-t4 .content-area")
);
await tm.open("vault1", "/notes/foo.md");
await tm.open("vault1", "/notes/bar.md");
// PaneTabManager.activate(tabId) is called inside open(), so the most
// recently opened tab is active by default.
let [t1, t2] = $$("#tl4 .tab-item");
assert.ok(!t1.classList.contains("active"), `t1 should NOT be active (it was opened first): ${t1.className}`);
assert.ok(t2.classList.contains("active"), `t2 should be active (most recent): ${t2.className}`);
// Now explicitly activate the first tab.
tm.activate("vault1::/notes/foo.md");
tm._renderTabs();
[t1, t2] = $$("#tl4 .tab-item");
assert.ok(t1.classList.contains("active"), "after explicit activate, t1 should be active");
assert.ok(!t2.classList.contains("active"), "t2 should no longer be active");
});
await test("close() removes the tab-item from DOM", async () => {
resetBody(`
<div id="pane-t5">
<div class="tab-bar"></div>
<div id="tl5" class="tab-list"></div>
<div class="content-area"></div>
</div>
`);
const tm = createPaneTabManager("t5");
tm.init(
document.querySelector("#pane-t5 .tab-bar"),
document.querySelector("#tl5"),
document.querySelector("#pane-t5 .content-area")
);
await tm.open("vault1", "/notes/foo.md");
await tm.open("vault1", "/notes/bar.md");
assert.equal($$("#tl5 .tab-item").length, 2);
await tm.close("vault1::/notes/foo.md");
assert.equal($$("#tl5 .tab-item").length, 1);
assert.match($$("#tl5 .tab-item")[0].textContent, /bar/);
});
await test("isolation: tabs in pane-A do not appear in pane-B", async () => {
resetBody(`
<div id="pane-A"><div class="tab-bar"></div><div id="tlA" class="tab-list"></div><div class="content-area"></div></div>
<div id="pane-B"><div class="tab-bar"></div><div id="tlB" class="tab-list"></div><div class="content-area"></div></div>
`);
const tmA = createPaneTabManager("A");
tmA.init(
document.querySelector("#pane-A .tab-bar"),
document.querySelector("#tlA"),
document.querySelector("#pane-A .content-area")
);
const tmB = createPaneTabManager("B");
tmB.init(
document.querySelector("#pane-B .tab-bar"),
document.querySelector("#tlB"),
document.querySelector("#pane-B .content-area")
);
await tmA.open("v1", "/only-in-a.md");
assert.equal($$("#tlA .tab-item").length, 1);
assert.equal($$("#tlB .tab-item").length, 0, "pane-B must be empty");
await tmB.open("v1", "/only-in-b.md");
assert.equal($$("#tlA .tab-item").length, 1, "pane-A unchanged");
assert.equal($$("#tlB .tab-item").length, 1);
});
await test("PaneManager singleton exposes expected methods", () => {
assert.equal(typeof PaneManager, "object");
assert.equal(PaneManager.isSplit(), false);
assert.deepEqual(PaneManager.panes, []);
assert.equal(typeof PaneManager.splitRight, "function");
assert.equal(typeof PaneManager.splitDown, "function");
assert.equal(typeof PaneManager.closePane, "function");
});
await test("DragEvent dispatch on tab-item does not crash", async () => {
resetBody(`
<div id="pane-d">
<div class="tab-bar"></div>
<div id="tld" class="tab-list"></div>
<div class="content-area"></div>
</div>
`);
const tm = createPaneTabManager("d");
tm.init(
document.querySelector("#pane-d .tab-bar"),
document.querySelector("#tld"),
document.querySelector("#pane-d .content-area")
);
await tm.open("v1", "/drag.md");
const tab = document.querySelector("#tld .tab-item");
assert.ok(tab, "tab should exist");
const dt = {
setData: () => {},
getData: () => "",
types: ["text/plain"],
};
const ds = new Event("dragstart", { bubbles: true, cancelable: true });
Object.defineProperty(ds, "dataTransfer", { value: dt });
tab.dispatchEvent(ds); // should not throw
const de = new Event("dragend", { bubbles: true });
Object.defineProperty(de, "dataTransfer", { value: dt });
tab.dispatchEvent(de); // should not throw
});
await test("getActiveTabManager / getActiveContentArea helpers exist", () => {
assert.equal(typeof getActiveTabManager, "function");
assert.equal(typeof getActiveContentArea, "function");
});
// ── Summary ─────────────────────────────────────────────────────────────────
console.log(`\n${passCount}/${testCount} pane-manager tests passed\n`);
process.exit(passCount === testCount ? 0 : 1);