#!/usr/bin/env node /** * ObsiGate — JSDOM tests for the collaboration module (ROADMAP #62). * * Covers the pure helpers of frontend/js/collab.js (text diff used to apply * remote Yjs changes, cursor URL building, avatar initials). The WebSocket / * Yjs runtime paths are exercised by the backend pytest suite. * * Usage: node tests/frontend/collab.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 (collab.js imports i18n.js which touches window) ──────── const dom = new JSDOM( `
Alice
`, { url: "https://example.com/vault", pretendToBeVisual: true }, ); const w = dom.window; globalThis.window = w; globalThis.document = w.document; Object.defineProperty(globalThis, "navigator", { value: w.navigator, configurable: true, writable: true, }); globalThis.localStorage = w.localStorage; globalThis.WebSocket = w.WebSocket || class {}; const collab = await import( pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "collab.js")).href ); const { computeTextDiff, initialOf, buildCollabUrl, formatPeerCount } = collab; let testCount = 0; let passCount = 0; function test(name, fn) { testCount++; try { fn(); console.log(` ✓ ${name}`); passCount++; } catch (e) { console.log(` ✗ ${name}`); console.log(` ${e.message}`); } } console.log("\n── collab.js unit tests (#62) ──\n"); test("computeTextDiff returns null for identical text", () => { assert.equal(computeTextDiff("abc", "abc"), null); }); test("computeTextDiff detects an insertion", () => { assert.deepEqual(computeTextDiff("ab", "aXb"), { from: 1, to: 1, insert: "X" }); }); test("computeTextDiff detects a deletion", () => { assert.deepEqual(computeTextDiff("abc", "ac"), { from: 1, to: 2, insert: "" }); }); test("computeTextDiff detects a replacement", () => { assert.deepEqual(computeTextDiff("hello world", "hello there"), { from: 6, to: 11, insert: "there", }); }); test("computeTextDiff handles empty originals", () => { assert.deepEqual(computeTextDiff("", "new"), { from: 0, to: 0, insert: "new" }); assert.deepEqual(computeTextDiff("old", ""), { from: 0, to: 3, insert: "" }); }); test("computeTextDiff only replaces the changed region", () => { const oldText = "line1\nline2\nline3"; const newText = "line1\nCHANGED\nline3"; const diff = computeTextDiff(oldText, newText); assert.equal(diff.from, 6); assert.equal(diff.insert, "CHANGED"); assert.equal(oldText.slice(0, diff.from) + diff.insert + oldText.slice(diff.to), newText); }); test("initialOf returns an uppercase initial", () => { assert.equal(initialOf("alice"), "A"); assert.equal(initialOf("Bob"), "B"); assert.equal(initialOf(""), "?"); assert.equal(initialOf(" "), "?"); }); test("buildCollabUrl encodes vault and path segments", () => { const loc = { protocol: "https:", host: "obsigate.local" }; assert.equal( buildCollabUrl(loc, "Mon Vault", "Projets/mon fichier.md"), "wss://obsigate.local/ws/collab/Mon%20Vault/Projets/mon%20fichier.md", ); }); test("buildCollabUrl uses ws:// for http origins", () => { const loc = { protocol: "http:", host: "localhost:8080" }; assert.equal( buildCollabUrl(loc, "V", "note.md"), "ws://localhost:8080/ws/collab/V/note.md", ); }); test("formatPeerCount is always a non-negative integer string", () => { assert.equal(formatPeerCount(0), "0"); assert.equal(formatPeerCount(5), "5"); assert.equal(formatPeerCount(-2), "0"); assert.equal(formatPeerCount(undefined), "0"); }); console.log(`\n${passCount}/${testCount} tests passed\n`); process.exit(passCount === testCount ? 0 : 1);