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
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
#!/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);
|
||||
Reference in New Issue
Block a user