big cmap refactoring
This commit is contained in:
@@ -3,21 +3,38 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
let editorFactory;
|
||||
|
||||
function fakeNode(attributes) {
|
||||
const state = { ...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() {}, redraw() {},
|
||||
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() {
|
||||
global.Element = class Element {};
|
||||
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: [],
|
||||
@@ -25,13 +42,13 @@ async function loadEditorFactory() {
|
||||
createElement: () => ({ style: {}, remove() {} })
|
||||
};
|
||||
global.window = {
|
||||
Cmap: null,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
requestAnimationFrame: (callback) => callback()
|
||||
};
|
||||
await import("../static/cmap/cmap-racket-wiki.js");
|
||||
return global.window.RacketWikiCmap;
|
||||
editorFactory = global.window.RacketWikiCmap;
|
||||
return editorFactory;
|
||||
}
|
||||
|
||||
test("back from a second nested submap returns exactly one map level", async () => {
|
||||
@@ -46,7 +63,7 @@ test("back from a second nested submap returns exactly one map level", async ()
|
||||
clientWidth: 1200, clientHeight: 800, scrollLeft: 0, scrollTop: 0,
|
||||
isConnected: true
|
||||
};
|
||||
const editor = factory.createEditor(canvas, { Cmap: () => map });
|
||||
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" },
|
||||
@@ -72,3 +89,168 @@ test("back from a second nested submap returns exactly one map level", async ()
|
||||
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/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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user