"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); let editorFactory; function fakeNode(attributes) { const state = { ...attributes, visible: true }; return { attr(value) { if (typeof value === "string") return state[value]; Object.assign(state, value); return this; }, onRendered() {}, onMove() {}, onMoveEnd() {}, visible(value) { if (value === undefined) return state.visible; state.visible = Boolean(value); return state.visible; }, redraw() {}, toFront() {}, remove() {}, element: () => null }; } async function loadEditorFactory() { if (editorFactory) return editorFactory; global.Element = class Element { constructor() { this.tagName = "DIV"; this.id = ""; this.classList = []; this.dataset = {}; } closest() { return null; } }; global.document = { currentScript: null, styleSheets: [], body: { append() {} }, createElement: () => ({ style: {}, remove() {} }) }; global.window = { setTimeout, clearTimeout, requestAnimationFrame: (callback) => callback() }; await import("../static/js/cmap/cmap-racket-wiki.js"); editorFactory = global.window.RacketWikiCmap; return editorFactory; } test("back from a second nested submap returns exactly one map level", async () => { const factory = await loadEditorFactory(); 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 first = editor.addItem({ id: 1, kind: "submap", label: "First level", separateMap: true, mapReference: { id: "first", title: "First level" }, x: 100, y: 100, width: 220, height: 70 }); first.submapInitialized = true; const second = editor.addItem({ id: 2, kind: "submap", label: "Second level", separateMap: true, mapReference: { id: "second", title: "Second level" }, parentSubmap: first, submapDepth: 1, x: 400, y: 200, width: 220, height: 70 }); second.submapInitialized = true; editor.openSubmapMap(first); editor.openSubmapMap(second); assert.equal(editor.activeMapRoot, second); assert.equal(editor.canStepBackWithinMap(), true, "the host must use local map history before navigating to a source CMap route"); editor.openParentMap(); assert.equal(editor.activeMapRoot, first); assert.equal(editor.canStepBackWithinMap(), false); }); test("single-click selects and double-click edits every kind of concept", async () => { const factory = await loadEditorFactory(); const map = { onSelection() {}, onActivation() {}, node: (attributes) => fakeNode(attributes) }; const canvas = { addEventListener() {}, querySelector: () => null, clientWidth: 1200, clientHeight: 800, scrollLeft: 0, scrollTop: 0, isConnected: true }; const edited = []; const openedPages = []; const openedMaps = []; const editor = factory.createEditor(canvas, { createDiagramEngine: () => map, onEditItem: (record) => edited.push(record), onOpenPage: (record) => openedPages.push(record), onOpenCmap: (record) => openedMaps.push(record) }); const ordinary = editor.addItem({ id: 1, label: "Ordinary" }); const page = editor.addItem({ id: 2, label: "Page", pageSlug: "page" }); const linkedMap = editor.addItem({ id: 3, label: "Linked map", cmapSlug: "other" }); const submap = editor.addItem({ id: 4, kind: "submap", label: "Group" }); editor.handleMapSelection(linkedMap.node, { target: new global.Element(), ctrlKey: false, metaKey: false, shiftKey: false }); assert.equal(editor.selected(), linkedMap); assert.deepEqual(openedMaps, []); editor.handleMapActivation(ordinary.node, { preventDefault() {} }); editor.handleMapActivation(page.node, { preventDefault() {} }); editor.handleMapActivation(linkedMap.node, { preventDefault() {} }); editor.handleMapActivation(submap.node, { preventDefault() {} }); assert.deepEqual(edited, [ordinary, page, linkedMap, submap]); assert.deepEqual(openedPages, []); assert.deepEqual(openedMaps, []); }); test("a linked CMap opens only through its arrow decorator", async () => { await loadEditorFactory(); const { CmapItemDecorator } = await import( "../static/js/cmap/view/cmap-item-decorator.js"); const listeners = new Map(); const linkedButton = { dataset: {}, addEventListener(type, listener) { listeners.set(type, listener); } }; const elementListeners = new Map(); const element = { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() {} }, setAttribute() {}, addEventListener(type, listener) { elementListeners.set(type, listener); }, querySelector(selector) { return selector === ".rw-cmap-open-linked" ? linkedButton : null; } }; const openedMaps = []; const editor = { labels: { relation: "Relation" }, selectedItems: new Set(), selectedItem: null, activeMapRoot: null, onOpenCmap: (record) => openedMaps.push(record), ensureCanvasExtent() {} }; const record = { id: 1, kind: "concept", label: "Linked map", cmapSlug: "other", node: { attr(name) { return { x: 10, y: 20, width: 120, height: 50 }[name]; } } }; const decorator = new CmapItemDecorator(editor); decorator.fitItemToContent = () => {}; decorator.decorateItem(record, element); assert.equal(elementListeners.has("click"), false, "the complete concept must not become a navigation target"); listeners.get("click")({ preventDefault() {}, stopPropagation() {} }); assert.deepEqual(openedMaps, [record]); }); test("collapsed submap descendants stay hidden after the first render frame", async () => { const factory = await loadEditorFactory(); const renderFrames = []; global.window.requestAnimationFrame = (callback) => renderFrames.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 submap = editor.addItem({ id: 1, kind: "submap", label: "Collapsed" }); submap.expanded = false; const child = editor.addItem({ id: 2, label: "Hidden child", parentSubmap: submap }); editor.applyCurrentContextLayout(); assert.equal(child.node.visible(), false); child.node.visible(true); for (const renderFrame of renderFrames.splice(0)) renderFrame(); assert.equal(child.node.visible(), false, "the post-render visibility pass hides a descendant exposed by initial rendering"); }); test("loading an unresolved connector retains it without a runtime error", async () => { const factory = await loadEditorFactory(); global.window.requestAnimationFrame = (callback) => 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 warnings = []; const originalWarn = console.warn; console.warn = (...values) => warnings.push(values); try { editor.loadDocument({ schemaVersion: 2, concepts: [{ id: "concept-one", label: "One" }], items: [{ id: 1, conceptId: "concept-one", kind: "concept", x: 20, y: 20, width: 120, height: 50 }], connectors: [{ id: 1, sourceId: 1, targetId: 999, hasArrow: true, lineColor: "#333", lineWidth: 2 }] }); } finally { console.warn = originalWarn; } assert.equal(editor.unresolvedConnectors.length, 1); assert.match(warnings[0][0], /relation retained without rendering/); });