#!/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( `
`, { 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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(`
`); 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);