#!/usr/bin/env node /** * ObsiGate — JSDOM integration tests for Excalidraw Viewer (ROADMAP #78 F2). * * Loads the actual excalidraw-viewer.js ES module in a JSDOM environment and * verifies iframe creation, the postMessage init flow, and export helpers. * * Usage: node tests/frontend/excalidraw-viewer.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.MessageEvent = w.MessageEvent; globalThis.localStorage = w.localStorage; globalThis.sessionStorage = w.sessionStorage; Object.defineProperty(globalThis, "navigator", { value: w.navigator, configurable: true, writable: true, }); // The viewer calls addEventListener on the iframe contentWindow's source check. // We stub window.postMessage (jsdom doesn't dispatch it). w.HTMLIFrameElement.prototype.postMessage = () => {}; // jsdom may not initialize iframe.sandbox for created elements; we define it // lazily so the viewer's sandbox.add() calls work. Object.defineProperty(w.HTMLIFrameElement.prototype, "sandbox", { configurable: true, get() { if (!this._sandboxList) { this._sandboxList = new Set(); const list = this._sandboxList; this._sandboxListAdd = (token) => { list.add(token); }; } return { add: (token) => this._sandboxListAdd(token), contains: (token) => this._sandboxList.has(token), }; }, }); // ── Helpers ───────────────────────────────────────────────────────────────── 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 ")}`); } } // ── Dynamic import (after globals are set) ───────────────────────────────── const mod = await import( pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "excalidraw-viewer.js")).href ); const { renderExcalidraw, notifyExcalidrawThemeChange, destroyExcalidrawEditor } = mod; // ── Test suite ────────────────────────────────────────────────────────────── console.log("\n── excalidraw-viewer.js JSDOM integration tests (#78 F2) ──\n"); await test("module exports renderExcalidraw + helpers", () => { assert.equal(typeof renderExcalidraw, "function"); assert.equal(typeof notifyExcalidrawThemeChange, "function"); assert.equal(typeof destroyExcalidrawEditor, "function"); }); await test("renderExcalidraw creates an iframe with sandbox + static src", () => { const container = document.getElementById("content-area"); const data = { is_excalidraw: true, vault: "TestVault", path: "diagram.excalidraw", excalidraw_data: { elements: [], appState: {}, files: {} }, }; renderExcalidraw(container, data, "TestVault", "diagram.excalidraw"); const iframe = container.querySelector("iframe"); assert.ok(iframe, "iframe should be created"); assert.ok(iframe.src.includes("/static/excalidraw-editor.html"), `src: ${iframe.src}`); assert.ok(iframe.sandbox.contains("allow-scripts"), "sandbox allow-scripts"); assert.ok(iframe.sandbox.contains("allow-same-origin"), "sandbox allow-same-origin"); assert.match(iframe.style.cssText, /100%/); }); await test("renderExcalidraw accepts compressed .excalidraw.md data", () => { const container = document.getElementById("content-area"); const data = { is_excalidraw: true, vault: "V", path: "diagram.excalidraw.md", excalidraw_data_compressed: "dummy", }; // Should not throw when building with compressed payload renderExcalidraw(container, data, "V", "diagram.excalidraw.md"); const iframe = container.querySelector("iframe"); assert.ok(iframe, "iframe created for .excalidraw.md"); }); await test("notifyExcalidrawThemeChange does not throw with no editors", () => { notifyExcalidrawThemeChange("dark"); }); await test("destroyExcalidrawEditor is idempotent (no-op for unknown id)", () => { destroyExcalidrawEditor("excalidraw-999"); }); console.log(`\n${passCount}/${testCount} excalidraw-viewer tests passed\n`); process.exit(passCount === testCount ? 0 : 1);