"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); function fakeElement() { return { className: "rw-cmap-item", style: {}, dataset: {}, querySelector: () => null, querySelectorAll: () => [], remove() {}, addEventListener() {}, classList: { add() {}, remove() {}, toggle() {} }, setAttribute() {}, removeAttribute() {} }; } function fakeNode(attributes) { const state = { ...attributes }; return { attr(value) { if (typeof value === "string") return state[value]; Object.assign(state, value); return this; }, onRendered() {}, onMove() {}, onMoveEnd() {}, visible() {}, redraw() {}, toFront() {}, remove() {}, element: () => null }; } function fakeLink(attributes) { const state = { ...attributes }; let source = null; let target = null; let connectionChangeHandler = null; return { attr(value) { if (typeof value === "string") return state[value]; Object.assign(state, value); return this; }, sourceNode(value) { if (arguments.length === 0) return source; source = value; return this; }, targetNode(value) { if (arguments.length === 0) return target; target = value; return this; }, onConnectionChange(handler) { connectionChangeHandler = handler; }, triggerConnectionChange(type, node) { if (type === "source") source = node; if (type === "target") target = node; connectionChangeHandler(this, type, node); }, onRendered() {}, visible() {}, straighten() {}, draggable() {}, redraw() {}, remove() {}, element: () => null }; } async function loadEditorFactory() { const previousFactory = global.window && global.window.RacketWikiCmap; global.Element = class Element {}; global.document = { currentScript: null, styleSheets: [], body: { append() {} }, createElement: () => fakeElement() }; global.window = { setTimeout, clearTimeout, requestAnimationFrame: (callback) => callback() }; await import("../static/cmap/cmap-racket-wiki.js"); global.window.RacketWikiCmap = global.window.RacketWikiCmap || previousFactory; return global.window.RacketWikiCmap; } test("a dragged connector endpoint survives collapsing and expanding its submap", async () => { const factory = await loadEditorFactory(); const links = []; const map = { onSelection() {}, onActivation() {}, node: (attributes) => fakeNode(attributes), link(attributes) { const link = fakeLink(attributes); links.push(link); return link; } }; const canvas = { addEventListener() {}, querySelector: () => null, clientWidth: 1200, clientHeight: 800, scrollLeft: 0, scrollTop: 0, isConnected: true }; const editor = factory.createEditor(canvas, { createDiagramEngine: () => map }); const submap = editor.addItem({ id: 1, kind: "submap", label: "Problemen en Symptomen LTS Model", x: 300, y: 200, width: 320, height: 70 }); submap.expanded = true; submap.submapInitialized = true; const problems = editor.addItem({ id: 2, kind: "phrase", label: "problemen", parentSubmap: submap, submapDepth: 1, x: 400, y: 300 }); const symptoms = editor.addItem({ id: 3, kind: "phrase", label: "Symptomen", parentSubmap: submap, submapDepth: 1, x: 400, y: 500 }); const concept = editor.addItem({ id: 4, kind: "concept", label: "Rob maakt veel fouten", parentSubmap: submap, submapDepth: 1, x: 800, y: 400 }); const connector = editor.addConnector(problems, concept, true, { id: 1 }); links[0].triggerConnectionChange("source", symptoms.node); assert.equal(connector.source, symptoms, "the editor model follows the endpoint dragged by the drawing library"); editor.toggleSubmap(submap, false); editor.toggleSubmap(submap, true); assert.equal(connector.source, symptoms); assert.equal(connector.link.sourceNode(), symptoms.node); assert.deepEqual(editor.toDocument().connectors, [{ id: 1, sourceId: symptoms.id, targetId: concept.id, hasArrow: true, lineColor: "#333", lineWidth: 2 }]); }); test("moving a concept also moves connected concepts that link to another CMap", async () => { const factory = await loadEditorFactory(); const map = { onSelection() {}, onActivation() {}, node: (attributes) => fakeNode(attributes), link: (attributes) => fakeLink(attributes) }; const canvas = { addEventListener() {}, querySelector: () => null, clientWidth: 1200, clientHeight: 800, scrollLeft: 0, scrollTop: 0, isConnected: true }; const editor = factory.createEditor(canvas, { createDiagramEngine: () => map }); const owner = editor.addItem({ id: 1, label: "Owner", x: 100, y: 100 }); const phrase = editor.addItem({ id: 2, kind: "phrase", label: "relates to", x: 260, y: 100 }); const linked = editor.addItem({ id: 3, label: "Linked CMap concept", cmapSlug: "other-map", x: 420, y: 100 }); editor.addConnector(owner, phrase, true, { id: 1 }); editor.addConnector(phrase, linked, true, { id: 2 }); editor.selectItem(owner); editor.handleItemMove(owner, 180, 140); assert.deepEqual({ owner: [owner.node.attr("x"), owner.node.attr("y")], phrase: [phrase.node.attr("x"), phrase.node.attr("y")], linked: [linked.node.attr("x"), linked.node.attr("y")] }, { owner: [180, 140], phrase: [340, 140], linked: [500, 140] }); }); test("moving a concept schedules one boundary-reference geometry refresh", async () => { const factory = await loadEditorFactory(); const frames = []; global.window.requestAnimationFrame = (callback) => frames.push(callback); const map = { onSelection() {}, onActivation() {}, node: (attributes) => fakeNode(attributes) }; const canvas = { addEventListener() {}, querySelector: () => null, clientWidth: 1200, clientHeight: 800, scrollLeft: 0, scrollTop: 0, isConnected: true }; const editor = factory.createEditor(canvas, { createDiagramEngine: () => map }); const concept = editor.addItem({ id: 1, label: "Inside concept", x: 100, y: 100 }); let refreshCount = 0; editor.refreshBoundaryReferences = () => { refreshCount += 1; }; editor.handleItemMove(concept, 150, 130); editor.handleItemMove(concept, 170, 140); assert.equal(frames.length, 1, "pointer moves in one frame are coalesced"); frames[0](); assert.equal(refreshCount, 1); });