"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); 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 }; } async function loadEditorFactory() { global.Element = class Element {}; global.document = { currentScript: null, styleSheets: [], body: { append() {} }, createElement: () => ({ style: {}, remove() {} }) }; global.window = { Cmap: null, setTimeout, clearTimeout, requestAnimationFrame: (callback) => callback() }; await import("../static/cmap/cmap-racket-wiki.js"); return global.window.RacketWikiCmap; } test("the last selected concept determines target size and alignment", 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, { Cmap: () => map }); const first = editor.addItem({ id: 1, label: "First", externalUrl: "https://example.com/first", x: 10, y: 10, width: 100, height: 40 }); const second = editor.addItem({ id: 2, label: "Second", x: 80, y: 100, width: 120, height: 45 }); const third = editor.addItem({ id: 3, label: "Third", x: 250, y: 250, width: 200, height: 60 }); assert.equal(editor.conceptRepository().concept(first.conceptId).label, "First"); assert.equal(editor.conceptMapModel().item(first.id).conceptId, first.conceptId); assert.equal("node" in editor.conceptRepository().concept(first.conceptId), false, "repository concepts never contain drawing nodes"); assert.equal("node" in editor.conceptMapModel().item(first.id), false, "map placements never contain drawing nodes"); const savedDocument = editor.toDocument(); assert.equal( savedDocument.concepts.find((concept) => concept.id === first.conceptId).externalUrl, "https://example.com/first", "external links are serialized as shared concept content" ); const reloaded = factory.createEditor(canvas, { Cmap: () => map }); reloaded.loadModel(editor.currentModel()); assert.equal( reloaded.toDocument().concepts.find((concept) => concept.id === first.conceptId).externalUrl, "https://example.com/first", "external links survive an editor model round-trip" ); editor.selectItem(first); editor.selectItem(third, { additive: true }); editor.selectItem(second, { additive: true }); editor.resetHistory(); assert.equal(editor.selected(), second, "the last selected concept is the target object"); assert.equal(editor.canLayoutSelection("distribute-horizontal"), true); assert.equal(editor.canLayoutSelection("distribute-vertical"), true); editor.applySelectionLayout("align-right"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("x"))), [100, 80, 0]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("align-center"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("x"))), [90, 80, 40]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("align-left"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("x"))), [80, 80, 80]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("align-bottom"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("y"))), [105, 100, 85]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("align-middle"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("y"))), [102.5, 100, 92.5]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("align-top"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("y"))), [100, 100, 100]); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("distribute-horizontal"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("x"))), [10, 120, 250], "outer items stay fixed and horizontal gaps become equal"); first.node.attr({ x: 10, y: 10 }); second.node.attr({ x: 80, y: 100 }); third.node.attr({ x: 250, y: 250 }); editor.applySelectionLayout("distribute-vertical"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("y"))), [10, 127.5, 250], "outer items stay fixed and the middle item receives an equal gap"); editor.applySelectionLayout("same-width"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("width"))), [120, 120, 120]); editor.applySelectionLayout("same-height"); assert.deepEqual([first, second, third].map((item) => Number(item.node.attr("height"))), [45, 45, 45]); first.node.attr({ width: 90, height: 35 }); third.node.attr({ width: 180, height: 70 }); editor.applySelectionLayout("same-size"); assert.deepEqual([first, second, third].map((item) => [Number(item.node.attr("width")), Number(item.node.attr("height"))]), [[120, 45], [120, 45], [120, 45]]); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(editor.canUndo(), true, "layout commands participate in undo and autosave history"); assert.deepEqual(editor.toDocument().items.map((item) => [item.width, item.height]), [[120, 45], [120, 45], [120, 45]]); });