refactoring of cmaps, widgets, etc.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
CmapAppearance,
|
||||
cmapFontSizeInPoints,
|
||||
normalizedCmapFontSize
|
||||
} from "../static/cmap/model/appearance.js";
|
||||
|
||||
const defaultValues = {
|
||||
backgroundColor: "#fff4cf", textColor: "#222222",
|
||||
fontFamily: "Arial", fontSize: 11, fontWeight: "700", fontStyle: "normal",
|
||||
synopsisTextColor: "#4d4d4d", synopsisFontFamily: "Arial",
|
||||
synopsisFontSize: 9, synopsisFontWeight: "400", synopsisFontStyle: "normal",
|
||||
submapBackgroundColor: "#edf7e8", submapBorderColor: "#57834a"
|
||||
};
|
||||
|
||||
function appearance() {
|
||||
return new CmapAppearance({
|
||||
styles: [{
|
||||
id: "default", nameKey: "style-default", protected: true, values: defaultValues
|
||||
}],
|
||||
palette: ["#ffffff", "#222222"]
|
||||
});
|
||||
}
|
||||
|
||||
test("CMap appearance owns detached styles and palette values", () => {
|
||||
const model = appearance();
|
||||
const styles = model.styles;
|
||||
const palette = model.palette;
|
||||
styles[0].values.backgroundColor = "#000000";
|
||||
palette[0] = "#000000";
|
||||
assert.equal(model.defaultValues.backgroundColor, "#fff4cf");
|
||||
assert.equal(model.palette[0], "#ffffff");
|
||||
});
|
||||
|
||||
test("CMap appearance normalizes form values and recognizes named styles", () => {
|
||||
const model = appearance();
|
||||
const values = model.normalizeValues({
|
||||
...defaultValues,
|
||||
backgroundColor: "#FFF4CF",
|
||||
fontSize: "11.2"
|
||||
});
|
||||
assert.equal(values.backgroundColor, "#fff4cf");
|
||||
assert.equal(values.fontSize, 11);
|
||||
assert.equal(model.matchingStyleId(values), "default");
|
||||
});
|
||||
|
||||
test("custom styles and palette colours change only through model methods", () => {
|
||||
const model = appearance();
|
||||
const style = model.putStyle({ id: "review", name: "Review", values: defaultValues });
|
||||
assert.equal(model.style("review").name, "Review");
|
||||
assert.equal(model.matchingStyleId(style.values), "default");
|
||||
assert.equal(model.deleteStyle("default"), false);
|
||||
assert.equal(model.deleteStyle("review"), true);
|
||||
assert.equal(model.setPaletteColor(1, "#ABCDEF"), true);
|
||||
assert.equal(model.palette[1], "#abcdef");
|
||||
assert.equal(model.setPaletteColor(1, "not-a-colour"), false);
|
||||
});
|
||||
|
||||
test("CMap appearance rejects incomplete backend aggregates", () => {
|
||||
assert.throws(
|
||||
() => new CmapAppearance({ styles: [], palette: ["#ffffff"] }),
|
||||
/default style and colour palette/);
|
||||
});
|
||||
|
||||
test("CMap font sizes convert and normalize to backend units", () => {
|
||||
assert.equal(cmapFontSizeInPoints("16px"), 12);
|
||||
assert.equal(cmapFontSizeInPoints("1.5em", 10), 15);
|
||||
assert.equal(normalizedCmapFontSize("55", 11), 54);
|
||||
});
|
||||
+46
-26
@@ -2,10 +2,19 @@
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { generateMarkdown, derivedDocument, relationLines } = require("../static/js/cmap-export.js");
|
||||
let CmapMarkdownExporter;
|
||||
|
||||
test.before(async () => {
|
||||
({ CmapMarkdownExporter } = await import(
|
||||
"../static/cmap/model/markdown-exporter.js"));
|
||||
});
|
||||
|
||||
function map(slug, title, document) {
|
||||
return { slug, title, document };
|
||||
return { slug, title, toDocument: () => document };
|
||||
}
|
||||
|
||||
function repository(maps) {
|
||||
return { load: async (slug) => maps[slug] };
|
||||
}
|
||||
|
||||
const root = map("root", "Root map", {
|
||||
@@ -52,13 +61,13 @@ const pages = new Map([
|
||||
]);
|
||||
|
||||
test("exports metadata, person tags, relations and linked maps to the selected depth", async () => {
|
||||
const markdown = await generateMarkdown({
|
||||
rootMap: root,
|
||||
const exporter = new CmapMarkdownExporter(
|
||||
repository({ root, child }),
|
||||
async (slug) => pages.get(slug));
|
||||
const markdown = await exporter.export(root, {
|
||||
maxDepth: 1,
|
||||
includeWikiPages: false,
|
||||
language: "nl",
|
||||
loadConceptMap: async (slug) => ({ root, child }[slug]),
|
||||
loadWikiPage: async (slug) => pages.get(slug)
|
||||
language: "nl"
|
||||
});
|
||||
|
||||
assert.match(markdown, /CMap: Root map \(niveau 0\)/);
|
||||
@@ -73,25 +82,25 @@ test("exports metadata, person tags, relations and linked maps to the selected d
|
||||
});
|
||||
|
||||
test("depth zero exports only the selected map", async () => {
|
||||
const markdown = await generateMarkdown({
|
||||
rootMap: root,
|
||||
const exporter = new CmapMarkdownExporter(
|
||||
repository({ root, child }),
|
||||
async (slug) => pages.get(slug));
|
||||
const markdown = await exporter.export(root, {
|
||||
maxDepth: 0,
|
||||
includeWikiPages: false,
|
||||
loadConceptMap: async (slug) => ({ root, child }[slug]),
|
||||
loadWikiPage: async (slug) => pages.get(slug)
|
||||
includeWikiPages: false
|
||||
});
|
||||
assert.match(markdown, /Root map/);
|
||||
assert.doesNotMatch(markdown, /## CMap: Child map/);
|
||||
});
|
||||
|
||||
test("optionally appends linked wiki pages and concept explanations", async () => {
|
||||
const markdown = await generateMarkdown({
|
||||
rootMap: root,
|
||||
const exporter = new CmapMarkdownExporter(
|
||||
repository({ root, child }),
|
||||
async (slug) => pages.get(slug));
|
||||
const markdown = await exporter.export(root, {
|
||||
maxDepth: 0,
|
||||
includeWikiPages: true,
|
||||
language: "en",
|
||||
loadConceptMap: async (slug) => ({ root, child }[slug]),
|
||||
loadWikiPage: async (slug) => pages.get(slug)
|
||||
language: "en"
|
||||
});
|
||||
assert.match(markdown, /## Linked wiki pages/);
|
||||
assert.match(markdown, /### Approach/);
|
||||
@@ -101,7 +110,7 @@ test("optionally appends linked wiki pages and concept explanations", async () =
|
||||
assert.match(markdown, /Because\./);
|
||||
});
|
||||
|
||||
test("derived views contain their root subtree and inherit useful root metadata", () => {
|
||||
test("derived views contain their root subtree and inherit useful root metadata", async () => {
|
||||
const source = {
|
||||
schemaVersion: 2,
|
||||
concepts: [
|
||||
@@ -119,17 +128,22 @@ test("derived views contain their root subtree and inherit useful root metadata"
|
||||
{ id: 2, sourceId: 11, targetId: 12 }
|
||||
]
|
||||
};
|
||||
const result = derivedDocument(source, {
|
||||
const derived = map("team", "Team", {
|
||||
derivedView: { sourceCmapSlug: "source", rootItemId: 10 }
|
||||
});
|
||||
assert.deepEqual(result.items.map((item) => item.id), [10, 11]);
|
||||
assert.deepEqual(result.connectors.map((connector) => connector.id), [1]);
|
||||
assert.deepEqual(result.metadata.tags, ["people"]);
|
||||
assert.equal(result.metadata.summary, "Team scope");
|
||||
assert.equal(result.metadata.explanationPageSlug, "cmap:team");
|
||||
const sourceMap = map("source", "Source", source);
|
||||
const exporter = new CmapMarkdownExporter(
|
||||
repository({ source: sourceMap }),
|
||||
async (slug) => ({ slug, title: slug, markdown: "Team explanation", tags: [] }));
|
||||
const markdown = await exporter.export(derived, { maxDepth: 0 });
|
||||
assert.match(markdown, /Team scope/);
|
||||
assert.match(markdown, /`people`/);
|
||||
assert.match(markdown, /#### Work/);
|
||||
assert.doesNotMatch(markdown, /#### Outside/);
|
||||
assert.match(markdown, /Team explanation/);
|
||||
});
|
||||
|
||||
test("direct and phrase relations receive readable Markdown", () => {
|
||||
test("direct and phrase relations receive readable Markdown", async () => {
|
||||
const documentValue = {
|
||||
items: [
|
||||
{ id: 1, label: "A" }, { id: 2, kind: "phrase", label: "supports" },
|
||||
@@ -141,5 +155,11 @@ test("direct and phrase relations receive readable Markdown", () => {
|
||||
{ sourceId: 3, targetId: 4, hasArrow: true }
|
||||
]
|
||||
};
|
||||
assert.deepEqual(relationLines(documentValue), ["A — **supports** → B", "B → C"]);
|
||||
const relationMap = map("relations", "Relations", documentValue);
|
||||
const exporter = new CmapMarkdownExporter(
|
||||
repository({ relations: relationMap }),
|
||||
async () => null);
|
||||
const markdown = await exporter.export(relationMap);
|
||||
assert.match(markdown, /A — \*\*supports\*\* → B/);
|
||||
assert.match(markdown, /B → C/);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ test.before(async () => {
|
||||
preparedMapDocument,
|
||||
attachmentUrls,
|
||||
replaceAttachmentUrls
|
||||
} = await import("../static/js/wiki/cmap/interchange.js"));
|
||||
} = await import("../static/cmap/model/interchange.js"));
|
||||
});
|
||||
|
||||
const conceptId = "24d27086-9f8b-4b57-a7fb-47b513277555";
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { CmapJsonExporter } from "../static/cmap/model/json-exporter.js";
|
||||
import { CmapJsonImporter } from "../static/cmap/model/json-importer.js";
|
||||
import {
|
||||
CmapRepository,
|
||||
StoredConceptMap
|
||||
} from "../static/cmap/model/cmap-repository.js";
|
||||
import { CmapModel } from "../static/cmap/model/concept-map.js";
|
||||
|
||||
const conceptId = "11111111-1111-4111-8111-111111111111";
|
||||
const rootMap = {
|
||||
slug: "root",
|
||||
title: "Root",
|
||||
document: {
|
||||
schemaVersion: 2,
|
||||
metadata: { explanationPageSlug: "root-page" },
|
||||
concepts: [{ id: conceptId, label: "Root concept" }],
|
||||
items: [{ id: 1, conceptId, kind: "concept", x: 10, y: 20 }],
|
||||
connectors: []
|
||||
}
|
||||
};
|
||||
const page = {
|
||||
slug: "root-page",
|
||||
title: "Root page",
|
||||
markdown: "",
|
||||
tags: []
|
||||
};
|
||||
const storedRootMap = new StoredConceptMap(rootMap, CmapModel.fromDocument(rootMap.document));
|
||||
const cmapRepository = { load: async () => { throw new Error("no linked map expected"); } };
|
||||
|
||||
test("CmapJsonExporter builds a complete bundle including attachments", async () => {
|
||||
const exporter = new CmapJsonExporter(
|
||||
cmapRepository,
|
||||
async () => page,
|
||||
async () => ({
|
||||
ok: true,
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () => Uint8Array.from([1, 2, 3]).buffer
|
||||
}),
|
||||
"test"
|
||||
);
|
||||
|
||||
const bundle = await exporter.export(storedRootMap);
|
||||
|
||||
assert.equal(bundle.generator, "test");
|
||||
assert.equal(bundle.pages[0].attachments[0].contentBase64, "AQID");
|
||||
});
|
||||
|
||||
test("CmapJsonImporter reports conflicts and imports pages, attachments and maps", async () => {
|
||||
const calls = [];
|
||||
const api = async (path, options = {}) => {
|
||||
calls.push({ path, options });
|
||||
if (path.endsWith("/upload")) return { url: "/uploads/root-page/imported-image.png" };
|
||||
if (path === "/api/pages" && options.method === "POST") return { currentVersion: 1 };
|
||||
if (path === "/api/cmaps" && options.method === "POST") {
|
||||
const body = JSON.parse(options.body);
|
||||
return {
|
||||
slug: body.slug,
|
||||
title: body.title,
|
||||
currentVersion: 1,
|
||||
document: body.document
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const importer = new CmapJsonImporter(
|
||||
new CmapRepository(api), api, (_key, fallback) => fallback);
|
||||
const exporter = new CmapJsonExporter(
|
||||
cmapRepository,
|
||||
async () => page,
|
||||
async () => ({
|
||||
ok: true,
|
||||
headers: { get: () => "image/png" },
|
||||
arrayBuffer: async () => Uint8Array.from([1, 2, 3]).buffer
|
||||
})
|
||||
);
|
||||
const bundle = await exporter.export(storedRootMap);
|
||||
const file = { size: 100, text: async () => JSON.stringify(bundle) };
|
||||
|
||||
const parsed = await importer.read(file);
|
||||
assert.equal(parsed.rootCmapSlug, "root");
|
||||
assert.deepEqual(importer.conflicts(parsed, [{ slug: "root-page" }], []), {
|
||||
pages: [parsed.pages[0]],
|
||||
conceptMaps: []
|
||||
});
|
||||
|
||||
const result = await importer.import(parsed, {
|
||||
pages: [], conceptMaps: [], summary: "Imported"
|
||||
});
|
||||
assert.equal(result.pagesCreated, 1);
|
||||
assert.equal(result.mapsCreated, 1);
|
||||
assert.equal(result.attachmentsImported, 1);
|
||||
assert.deepEqual(calls.map((call) => `${call.options.method} ${call.path}`), [
|
||||
"POST /api/pages",
|
||||
"POST /api/pages/root-page/upload",
|
||||
"PUT /api/pages/root-page",
|
||||
"POST /api/cmaps"
|
||||
]);
|
||||
const pageUpdate = JSON.parse(calls[2].options.body);
|
||||
assert.match(pageUpdate.markdown, /imported-image\.png/);
|
||||
});
|
||||
+16
-14
@@ -14,7 +14,7 @@ import {
|
||||
ConceptMapConnector,
|
||||
ConceptMapPhrase
|
||||
} from "../static/cmap/model/concept-map.js";
|
||||
import { buildBundle } from "../static/js/wiki/cmap/interchange.js";
|
||||
import { buildBundle } from "../static/cmap/model/interchange.js";
|
||||
|
||||
const firstId = "11111111-1111-4111-8111-111111111111";
|
||||
const secondId = "22222222-2222-4222-8222-222222222222";
|
||||
@@ -217,33 +217,35 @@ test("an embedded submap becomes an independent map linked from its owner", () =
|
||||
|
||||
const extraction = CmapModel.fromDocument(document)
|
||||
.extractSubmap(1, "nested-subject");
|
||||
const parentDocument = extraction.parentModel.toDocument();
|
||||
const childDocument = extraction.childModel.toDocument();
|
||||
|
||||
assert.deepEqual(extraction.parentDocument.items.map((item) => item.id), [1, 4]);
|
||||
assert.equal(extraction.parentDocument.items[0].kind, "concept");
|
||||
assert.equal(extraction.parentDocument.concepts
|
||||
assert.deepEqual(parentDocument.items.map((item) => item.id), [1, 4]);
|
||||
assert.equal(parentDocument.items[0].kind, "concept");
|
||||
assert.equal(parentDocument.concepts
|
||||
.find((concept) => concept.id === firstId).cmapSlug, "nested-subject");
|
||||
assert.deepEqual(extraction.parentDocument.connectors.map((connector) => ({
|
||||
assert.deepEqual(parentDocument.connectors.map((connector) => ({
|
||||
sourceId: connector.sourceId,
|
||||
targetId: connector.targetId
|
||||
})), [{ sourceId: 4, targetId: 1 }]);
|
||||
assert.deepEqual(extraction.parentDocument.conceptOwnerships, [
|
||||
assert.deepEqual(parentDocument.conceptOwnerships, [
|
||||
{ parentConceptId: firstId, childConceptId: thirdId }
|
||||
]);
|
||||
|
||||
assert.deepEqual(extraction.childDocument.items.map((item) => item.id), [2, 3]);
|
||||
assert.ok(extraction.childDocument.items.every((item) => item.parentSubmapId === null));
|
||||
assert.deepEqual(extraction.childDocument.concepts.map((concept) => concept.id), [secondId]);
|
||||
assert.deepEqual(extraction.childDocument.connectors.map((connector) => connector.id), [1]);
|
||||
assert.equal(extraction.childDocument.conceptOwnerships, undefined);
|
||||
assert.equal(extraction.childDocument.derivedView, undefined);
|
||||
assert.equal(extraction.childDocument.metadata.summary, "Nested subject");
|
||||
assert.deepEqual(childDocument.items.map((item) => item.id), [2, 3]);
|
||||
assert.ok(childDocument.items.every((item) => item.parentSubmapId === null));
|
||||
assert.deepEqual(childDocument.concepts.map((concept) => concept.id), [secondId]);
|
||||
assert.deepEqual(childDocument.connectors.map((connector) => connector.id), [1]);
|
||||
assert.equal(childDocument.conceptOwnerships, undefined);
|
||||
assert.equal(childDocument.derivedView, undefined);
|
||||
assert.equal(childDocument.metadata.summary, "Nested subject");
|
||||
|
||||
const convertedDerivedMap = CmapModel.fromDocument(document).extractSubmap(
|
||||
1,
|
||||
"nested-subject",
|
||||
{ tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: "" }
|
||||
);
|
||||
assert.deepEqual(convertedDerivedMap.childDocument.metadata, {
|
||||
assert.deepEqual(convertedDerivedMap.childModel.metadata(), {
|
||||
tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: ""
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { PeopleRepository } from "../static/cmap/model/people-repository.js";
|
||||
|
||||
test("PeopleRepository keeps people API routes behind its public methods", async () => {
|
||||
const calls = [];
|
||||
const api = async (path, options = {}) => {
|
||||
calls.push({ path, options });
|
||||
if (!options.method) return { people: [{ id: 1, name: "Ada", active: true }] };
|
||||
return { id: 1, name: "Ada", active: true };
|
||||
};
|
||||
const repository = new PeopleRepository(api);
|
||||
|
||||
const people = await repository.all();
|
||||
const created = await repository.create("Ada");
|
||||
await repository.update(created, false);
|
||||
|
||||
assert.equal(people[0].name, "Ada");
|
||||
assert.deepEqual(calls.map((call) => call.path), [
|
||||
"/api/people",
|
||||
"/api/people",
|
||||
"/api/people/1"
|
||||
]);
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body), { name: "Ada" });
|
||||
assert.deepEqual(JSON.parse(calls[2].options.body), { name: "Ada", active: false });
|
||||
});
|
||||
|
||||
test("PeopleRepository normalizes a missing people collection to an empty list", async () => {
|
||||
const repository = new PeopleRepository(async () => ({}));
|
||||
assert.deepEqual(await repository.all(), []);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
CmapRepository,
|
||||
StoredConceptMap
|
||||
} from "../static/cmap/model/cmap-repository.js";
|
||||
import { CmapModel } from "../static/cmap/model/concept-map.js";
|
||||
|
||||
const documentValue = {
|
||||
schemaVersion: 2,
|
||||
concepts: [{ id: "concept-1", label: "Legacy concept" }],
|
||||
items: [{ id: 1, conceptId: "concept-1", kind: "concept", x: 10, y: 20 }],
|
||||
connectors: []
|
||||
};
|
||||
|
||||
test("CmapRepository decodes backend documents and normalizes legacy concept ids", async () => {
|
||||
const repository = new CmapRepository(async () => ({
|
||||
slug: "roadmap",
|
||||
title: "Roadmap",
|
||||
currentVersion: 3,
|
||||
document: JSON.stringify(JSON.stringify(documentValue))
|
||||
}));
|
||||
|
||||
const storedMap = await repository.load("roadmap");
|
||||
|
||||
assert.ok(storedMap instanceof StoredConceptMap);
|
||||
assert.ok(storedMap.model instanceof CmapModel);
|
||||
assert.equal(storedMap.currentVersion, 3);
|
||||
assert.equal(storedMap.toDocument().concepts[0].id, "legacy:roadmap:concept-1");
|
||||
assert.equal(storedMap.toDocument().items[0].conceptId, "legacy:roadmap:concept-1");
|
||||
});
|
||||
|
||||
test("CmapRepository serializes model saves with backend version information", async () => {
|
||||
const calls = [];
|
||||
const api = async (path, options = {}) => {
|
||||
calls.push({ path, options });
|
||||
return {
|
||||
slug: "roadmap",
|
||||
title: "Roadmap",
|
||||
currentVersion: calls.length,
|
||||
document: documentValue
|
||||
};
|
||||
};
|
||||
const repository = new CmapRepository(api);
|
||||
const storedMap = await repository.load("roadmap");
|
||||
const savedMap = await repository.save(storedMap, storedMap.model, {
|
||||
summary: "Manual save",
|
||||
saveKind: "manual",
|
||||
snapshot: true
|
||||
});
|
||||
|
||||
assert.equal(savedMap.currentVersion, 2);
|
||||
assert.equal(calls[1].path, "/api/cmaps/roadmap");
|
||||
const body = JSON.parse(calls[1].options.body);
|
||||
assert.equal(body.baseVersion, 1);
|
||||
assert.equal(body.summary, "Manual save");
|
||||
assert.equal(body.saveKind, "manual");
|
||||
assert.equal(body.snapshot, true);
|
||||
assert.deepEqual(body.document.concepts, storedMap.toDocument().concepts);
|
||||
});
|
||||
|
||||
test("CmapRepository keeps CMap routes behind its public storage API", async () => {
|
||||
const calls = [];
|
||||
const api = async (path, options = {}) => {
|
||||
calls.push({ path, options });
|
||||
if (path === "/api/cmaps") {
|
||||
if (!options.method) return { conceptMaps: [{ slug: "one", title: "One" }] };
|
||||
return { slug: "one", title: "One", currentVersion: 1, document: documentValue };
|
||||
}
|
||||
if (path.endsWith("/history")) return { versions: [{ version: 1 }] };
|
||||
if (path === "/api/cmaps/concept-usage") return { placements: [{ conceptId: "one" }] };
|
||||
return { slug: "one", title: "One", currentVersion: 2, document: documentValue };
|
||||
};
|
||||
const repository = new CmapRepository(api);
|
||||
const summaries = await repository.list();
|
||||
const storedMap = await repository.create("One", CmapModel.fromDocument(documentValue), "one");
|
||||
const versions = await repository.history(storedMap);
|
||||
const placements = await repository.conceptUsage();
|
||||
await repository.deleteVersion(storedMap, 1);
|
||||
await repository.archive(storedMap, "One");
|
||||
|
||||
assert.equal(summaries[0].slug, "one");
|
||||
assert.equal(versions[0].version, 1);
|
||||
assert.equal(placements[0].conceptId, "one");
|
||||
assert.ok(calls.some((call) => call.path === "/api/cmaps/one/versions/1" &&
|
||||
call.options.method === "DELETE"));
|
||||
assert.ok(calls.some((call) => call.path === "/api/cmaps/one" &&
|
||||
call.options.method === "DELETE"));
|
||||
});
|
||||
@@ -66,11 +66,11 @@ test("the last selected concept determines target size and alignment", async ()
|
||||
"external links are serialized as shared concept content"
|
||||
);
|
||||
const reloaded = factory.createEditor(canvas, { Cmap: () => map });
|
||||
reloaded.loadDocument(savedDocument);
|
||||
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 save and reload round-trip"
|
||||
"external links survive an editor model round-trip"
|
||||
);
|
||||
editor.selectItem(first);
|
||||
editor.selectItem(third, { additive: true });
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { CmapAppearance } from "../static/cmap/model/appearance.js";
|
||||
import { CmapAppearanceRepository } from "../static/cmap/model/appearance-repository.js";
|
||||
import { CmapSettingsRepository } from "../static/cmap/model/settings-repository.js";
|
||||
|
||||
test("appearance repository owns the complete backend appearance envelope", async () => {
|
||||
const requests = [];
|
||||
const appearance = {
|
||||
styles: [{
|
||||
id: "default",
|
||||
nameKey: "style-default",
|
||||
protected: true,
|
||||
values: {
|
||||
backgroundColor: "#fff4cf", textColor: "#222222",
|
||||
fontFamily: "Arial", fontSize: 11, fontWeight: "700", fontStyle: "normal",
|
||||
synopsisTextColor: "#4d4d4d", synopsisFontFamily: "Arial",
|
||||
synopsisFontSize: 9, synopsisFontWeight: "400", synopsisFontStyle: "normal",
|
||||
submapBackgroundColor: "#edf7e8", submapBorderColor: "#57834a"
|
||||
}
|
||||
}],
|
||||
palette: ["#ffffff", "#f1f3f5"]
|
||||
};
|
||||
const repository = new CmapAppearanceRepository(async (route, options = {}) => {
|
||||
requests.push({ route, options });
|
||||
return appearance;
|
||||
});
|
||||
|
||||
const loaded = await repository.load();
|
||||
assert.ok(loaded instanceof CmapAppearance);
|
||||
assert.deepEqual(loaded.toData(), appearance);
|
||||
const stored = await repository.save(loaded);
|
||||
assert.equal(stored, loaded);
|
||||
assert.deepEqual(stored.toData(), appearance);
|
||||
assert.equal(requests[0].route, "/api/cmap-appearance");
|
||||
assert.equal(requests[1].options.method, "PUT");
|
||||
assert.deepEqual(JSON.parse(requests[1].options.body), appearance);
|
||||
});
|
||||
|
||||
test("settings repository keeps backend settings in session memory", async () => {
|
||||
const requests = [];
|
||||
const repository = new CmapSettingsRepository(async (route, options = {}) => {
|
||||
requests.push({ route, options });
|
||||
if (route === "/api/cmap-settings") {
|
||||
return {
|
||||
startCmapSlug: "architecture",
|
||||
pageGuidesVisible: false,
|
||||
zooms: [{ cmapSlug: "architecture", contextKey: "root", zoomPercent: 125 }]
|
||||
};
|
||||
}
|
||||
if (route.endsWith("/start")) return { startCmapSlug: "review" };
|
||||
if (route.endsWith("/page-guides")) return { pageGuidesVisible: true };
|
||||
return { zoomPercent: 150 };
|
||||
});
|
||||
|
||||
await repository.load();
|
||||
assert.equal(repository.startCmapSlug, "architecture");
|
||||
assert.equal(repository.pageGuidesVisible, false);
|
||||
assert.equal(repository.zoom("architecture", "root"), 125);
|
||||
assert.equal(repository.zoom("architecture", "unknown"), 100);
|
||||
|
||||
assert.equal(await repository.setStartCmap("review"), "review");
|
||||
assert.equal(await repository.setPageGuidesVisible(true), true);
|
||||
assert.equal(await repository.setZoom("architecture", "root", 150), 150);
|
||||
assert.equal(repository.zoom("architecture", "root"), 150);
|
||||
assert.equal(requests.length, 4);
|
||||
});
|
||||
@@ -49,6 +49,23 @@ test("WikiWords use page and CMap catalogues without expanding protected text",
|
||||
"[Homeopathie Wiki](#/homeopathie) en [Architectuur Kaart](#cmap/architectuur), maar `NieuwePagina` niet.");
|
||||
});
|
||||
|
||||
test("an exclamation mark renders a WikiWord literally without becoming visible", () => {
|
||||
const pages = [
|
||||
{ slug: "homeopathie", namespace: "", pageSlug: "homeopathie", title: "Homeopathie Wiki" }
|
||||
];
|
||||
const conceptMaps = [{ slug: "architectuur", title: "Architectuur Kaart" }];
|
||||
const markdown = "!HomeopathieWiki en !NieuwePagina en !cmap:ArchitectuurKaart.";
|
||||
const expanded = expandWikiMentions(markdown, pages, [], conceptMaps);
|
||||
assert.equal(expanded,
|
||||
"HomeopathieWiki en NieuwePagina en cmap:ArchitectuurKaart.");
|
||||
assert.equal(expandWikiMentions(expanded, pages, [], conceptMaps), expanded);
|
||||
});
|
||||
|
||||
test("the WikiWord escape remains literal in protected Markdown", () => {
|
||||
const markdown = "`!NieuwePagina` en [!NieuwePagina](https://example.com)";
|
||||
assert.equal(expandWikiMentions(markdown, [], [], []), markdown);
|
||||
});
|
||||
|
||||
test("ambiguous CMap mentions are not resolved", () => {
|
||||
const conceptMaps = [
|
||||
{ slug: "first", title: "Zelfde Kaart" },
|
||||
|
||||
Reference in New Issue
Block a user