feat: collaboration temps reel - edition simultanee (#62)
CI / lint (push) Successful in 1m1s
CI / security (push) Successful in 41s
CI / test (push) Successful in 1m47s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 10m36s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 1m1s
CI / security (push) Successful in 41s
CI / test (push) Successful in 1m47s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 10m36s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
WebSocket /ws/collab/{vault}/{path} (rooms par fichier), relais Yjs/CRDT, awareness (curseurs colores + presence), persistance serveur debounce 2s, auth WS + check_vault_access, reconnexion automatique. Frontend frontend/js/collab.js, backend backend/collab.py. Tests: 17 backend (5 clients simultanes) + 10 frontend. Docs: CHANGELOG, ROADMAP, fiche features/collaboration.md, README FR/EN.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#!/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(
|
||||
`<!DOCTYPE html><html><body>
|
||||
<div class="user-display-name">Alice</div>
|
||||
<div id="collab-presence" style="display:none"></div>
|
||||
</body></html>`,
|
||||
{ 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);
|
||||
Reference in New Issue
Block a user