- 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
272 lines
9.5 KiB
JavaScript
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);
|