Files
racket-wiki/test/cmap-interchange.test.js
T

231 lines
9.0 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
let buildBundle;
let validateBundle;
let preparedMapDocument;
let attachmentUrls;
let replaceAttachmentUrls;
test.before(async () => {
({
buildBundle,
validateBundle,
preparedMapDocument,
attachmentUrls,
replaceAttachmentUrls
} = await import("../static/js/cmap/model/interchange.js"));
});
const conceptId = "24d27086-9f8b-4b57-a7fb-47b513277555";
const childId = "53ef1cf3-c936-48f7-889a-ad644fab9321";
const styledItem = {
id: 10, conceptId, kind: "concept", x: 127.5, y: 88, width: 312,
height: 97, backgroundColor: "#ffe5a8", borderColor: "#a97c00",
textColor: "#172033", fontFamily: "Georgia", fontSize: 13.5,
fontWeight: "700", synopsisFontSize: 9.5
};
const root = {
slug: "architecture",
title: "Architecture",
document: {
schemaVersion: 2,
metadata: { explanationPageSlug: "cmap:architecture" },
concepts: [{
id: conceptId, label: "Process", synopsis: "Shared content",
aspects: ["TODO"], pageSlug: "process:details", cmapSlug: "child",
externalUrl: "https://example.com/process"
}],
items: [styledItem, { id: 11, kind: "phrase", label: "contains", x: 500, y: 90 }],
connectors: [{ id: 1, sourceId: 10, targetId: 11, hasArrow: true }]
}
};
const child = {
slug: "child", title: "Child", document: {
schemaVersion: 2,
concepts: [{ id: childId, label: "Child concept" }],
items: [{ id: 1, conceptId: childId, kind: "concept", x: 20, y: 30 }],
connectors: []
}
};
const pages = new Map([
["cmap:architecture", { slug: "cmap:architecture", title: "Explanation", markdown: "# Explanation", tags: ["cmap"] }],
["process:details", {
slug: "process:details", title: "Details",
markdown: "Full page\n\n![Diagram](/uploads/process:details/1700000-diagram.png)", tags: []
}]
]);
const exportedAttachment = {
mimeType: "image/png",
contentBase64: "aW1hZ2U="
};
const loadAttachment = async (url) => {
assert.equal(url, "/uploads/process:details/1700000-diagram.png");
return exportedAttachment;
};
test("bundle separates shared content while preserving complete diagram layout", async () => {
const bundle = await buildBundle({
rootMap: root,
maxDepth: 1,
exportedAt: "2026-08-25T12:00:00.000Z",
generator: "test",
loadConceptMap: async (slug) => ({ child }[slug]),
loadWikiPage: async (reference) => pages.get(reference),
loadAttachment
});
assert.equal(bundle.format, "racket-wiki-cmap-bundle");
assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["architecture", "child"]);
assert.deepEqual(bundle.pages.map((page) => page.reference), ["cmap:architecture", "process:details"]);
assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).synopsis, "Shared content");
assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).externalUrl,
"https://example.com/process");
const exportedItem = bundle.cmaps[0].document.items[0];
assert.equal(exportedItem.x, 127.5);
assert.equal(exportedItem.fontSize, 13.5);
assert.equal(exportedItem.backgroundColor, "#ffe5a8");
assert.equal(exportedItem.synopsis, undefined, "shared content is not duplicated on a placement");
assert.deepEqual(bundle.cmaps[0].document.connectors, root.document.connectors);
assert.deepEqual(bundle.pages.find((page) => page.reference === "process:details").attachments, [{
url: "/uploads/process:details/1700000-diagram.png",
name: "1700000-diagram.png",
mimeType: "image/png",
contentBase64: "aW1hZ2U="
}]);
});
test("prepared import document rejoins concepts and placements without changing layout", async () => {
const bundle = await buildBundle({
rootMap: root, maxDepth: 0,
loadConceptMap: async () => { throw new Error("not loaded"); },
loadWikiPage: async (reference) => pages.get(reference),
loadAttachment
});
const documentValue = preparedMapDocument(bundle, bundle.cmaps[0]);
assert.equal(documentValue.concepts[0].synopsis, "Shared content");
assert.equal(documentValue.concepts[0].externalUrl, "https://example.com/process");
assert.equal(documentValue.items[0].width, 312);
assert.equal(documentValue.items[0].fontFamily, "Georgia");
assert.deepEqual(documentValue.connectors, root.document.connectors);
});
test("a derived CMap always carries its source map as a layout dependency", async () => {
const derived = {
slug: "derived", title: "Derived", document: {
schemaVersion: 2,
derivedView: { sourceCmapSlug: "architecture", rootItemId: 10 },
concepts: [], items: [], connectors: []
}
};
const bundle = await buildBundle({
rootMap: derived, maxDepth: 0,
loadConceptMap: async (slug) => {
assert.equal(slug, "architecture");
return root;
},
loadWikiPage: async (reference) => pages.get(reference),
loadAttachment
});
assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["derived", "architecture"]);
assert.equal(bundle.rootCmapSlug, "derived");
assert.equal(bundle.cmaps[1].document.items[0].x, 127.5);
});
test("temporary ids are accepted only when their concepts are placed", () => {
const bundle = {
format: "racket-wiki-cmap-bundle", formatVersion: 1,
rootCmapSlug: "generated", pages: [], missing: { cmaps: [], pages: [] },
concepts: [{ id: "new:generated-concept", label: "Generated concept" }],
cmaps: [{ slug: "generated", title: "Generated", document: {
concepts: [{ id: "new:generated-concept" }],
items: [{ id: 1, conceptId: "new:generated-concept", kind: "concept", x: 10, y: 20 }],
connectors: []
}}]
};
assert.equal(validateBundle(bundle), bundle);
const orphan = structuredClone(bundle);
orphan.cmaps[0].document.items = [];
assert.throws(() => validateBundle(orphan), /must occur as a diagram placement/);
});
test("invalid connector endpoints and duplicate slugs are rejected", () => {
const bundle = {
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
concepts: [{ id: conceptId, label: "Concept" }], pages: [],
cmaps: [
{ slug: "map", title: "Map", document: { concepts: [{ id: conceptId }], items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: [{ sourceId: 1, targetId: 99 }] } },
{ slug: "map", title: "Duplicate", document: { concepts: [], items: [], connectors: [] } }
]
};
assert.throws(() => validateBundle(bundle), /unknown item[\s\S]*duplicated/);
});
test("bundle export omits connectors whose endpoint no longer exists", async () => {
const mapWithDanglingConnectors = structuredClone(root);
mapWithDanglingConnectors.document.connectors.push(
{ id: 2, sourceId: 10, targetId: 99 },
{ id: 3, sourceId: 98, targetId: 11 }
);
const bundle = await buildBundle({
rootMap: mapWithDanglingConnectors,
maxDepth: 0,
loadConceptMap: async () => { throw new Error("not loaded"); },
loadWikiPage: async (reference) => pages.get(reference),
loadAttachment
});
assert.deepEqual(bundle.cmaps[0].document.connectors, root.document.connectors);
});
test("external concept links only accept complete http or https URLs", () => {
const bundle = {
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
concepts: [{ id: conceptId, label: "Concept", externalUrl: "javascript:alert(1)" }],
pages: [],
cmaps: [{ slug: "map", title: "Map", document: {
concepts: [{ id: conceptId }],
items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: []
}}]
};
assert.throws(() => validateBundle(bundle), /externalUrl: must be a complete http or https URL/);
bundle.concepts[0].externalUrl = "https://example.com/path";
assert.equal(validateBundle(bundle), bundle);
});
test("attachment URLs are detected once and can be rewritten after upload", () => {
const oldUrl = "/uploads/process:details/1700000-diagram.png";
const markdown = `![Diagram](${oldUrl})\n<img src="${oldUrl}">`;
assert.deepEqual(attachmentUrls(markdown), [oldUrl]);
assert.equal(
replaceAttachmentUrls(markdown, new Map([[oldUrl, "/uploads/process:details/new-diagram.png"]])),
"![Diagram](/uploads/process:details/new-diagram.png)\n" +
"<img src=\"/uploads/process:details/new-diagram.png\">"
);
assert.deepEqual(
attachmentUrls("![Diagram](/uploads/page/1700000-system overview.png)"),
["/uploads/page/1700000-system overview.png"]
);
});
test("invalid embedded attachment content is rejected", () => {
const bundle = {
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
concepts: [{ id: conceptId, label: "Concept" }],
cmaps: [{ slug: "map", title: "Map", document: {
concepts: [{ id: conceptId }],
items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: []
}}],
pages: [{
reference: "page", title: "Page", markdown: "", tags: [],
attachments: [{
url: "/uploads/page/file.png", name: "file.png",
mimeType: "image/png", contentBase64: "not base64!"
}]
}]
};
assert.throws(() => validateBundle(bundle), /contentBase64: must be valid base64/);
});