refactoring

This commit is contained in:
2026-09-02 08:38:03 +02:00
parent 649ff0d7c5
commit 38f255c1a4
48 changed files with 6998 additions and 5577 deletions
+274
View File
@@ -0,0 +1,274 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
Concept,
ConceptOwnership,
ConceptRelation,
ConceptRepository
} from "../static/cmap/model/concept-repository.js";
import {
CmapModel,
ConceptMap,
ConceptMapConcept,
ConceptMapConnector,
ConceptMapPhrase
} from "../static/cmap/model/concept-map.js";
import { buildBundle } from "../static/js/wiki/cmap/interchange.js";
const firstId = "11111111-1111-4111-8111-111111111111";
const secondId = "22222222-2222-4222-8222-222222222222";
const thirdId = "33333333-3333-4333-8333-333333333333";
function sampleDocument() {
return {
schemaVersion: 2,
metadata: {
tags: ["architecture"],
summary: "System overview",
explanationPageSlug: "cmap:architecture"
},
concepts: [{
id: firstId,
label: "Application",
synopsis: "Runs the application",
aspects: ["runtime"],
tags: [{ type: "person", value: "Hans" }],
descriptionPageSlug: "cmap:application",
pageSlug: "application",
cmapSlug: null,
externalUrl: "https://example.com/application",
imageSource: ""
}],
items: [{
id: 1,
conceptId: firstId,
kind: "concept",
parentCmapLink: false,
groupId: null,
childMap: null,
parentSubmapId: null,
submapDepth: 0,
expanded: false,
submapInitialized: false,
separateMap: false,
mapReference: null,
hiddenContexts: [],
layouts: {},
backgroundColor: "#ffffff",
borderColor: "#333333",
submapBackgroundColor: null,
submapBorderColor: null,
textColor: "#111111",
fontFamily: "Arial",
fontSize: "11pt",
fontWeight: "700",
fontStyle: "normal",
synopsisTextColor: "#444444",
synopsisFontFamily: "Arial",
synopsisFontSize: "9pt",
synopsisFontWeight: "400",
synopsisFontStyle: "normal",
width: 220,
height: 70,
autoWidth: false,
autoHeight: false,
x: 80,
y: 90
}, {
id: 2,
conceptId: null,
kind: "phrase",
label: "uses",
synopsis: "",
parentCmapLink: false,
groupId: null,
childMap: null,
parentSubmapId: null,
submapDepth: 0,
expanded: false,
submapInitialized: false,
separateMap: false,
mapReference: null,
hiddenContexts: [],
layouts: {},
backgroundColor: "#ffffff",
borderColor: "transparent",
submapBackgroundColor: null,
submapBorderColor: null,
textColor: "#111111",
fontFamily: "Arial",
fontSize: "11pt",
fontWeight: "700",
fontStyle: "normal",
synopsisTextColor: "#444444",
synopsisFontFamily: "Arial",
synopsisFontSize: "9pt",
synopsisFontWeight: "400",
synopsisFontStyle: "normal",
width: 145,
height: 36,
autoWidth: true,
autoHeight: true,
x: 330,
y: 100
}],
connectors: [{
id: 1,
sourceId: 1,
targetId: 2,
hasArrow: false,
lineColor: "#333",
lineWidth: 2
}],
conceptMaps: []
};
}
test("CmapModel round-trips repository content, placements, phrases and connectors", () => {
const document = sampleDocument();
const model = CmapModel.fromDocument(document);
assert.ok(model.repository.concept(firstId) instanceof Concept);
assert.ok(model.conceptMap.item(1) instanceof ConceptMapConcept);
assert.ok(model.conceptMap.item(2) instanceof ConceptMapPhrase);
assert.ok(model.conceptMap.connector(1) instanceof ConceptMapConnector);
assert.deepEqual(model.toDocument(), document);
});
test("two placements share one repository concept without sharing appearance", () => {
const model = CmapModel.fromDocument(sampleDocument());
model.conceptMap.addItem(new ConceptMapConcept(3, firstId, {
x: 600, y: 300, width: 180, height: 60, backgroundColor: "#ffeeaa"
}));
model.repository.concept(firstId).update({ label: "Web application" });
assert.equal(model.repository.concept(firstId).label, "Web application");
assert.equal(model.conceptMap.item(1).backgroundColor, "#ffffff");
assert.equal(model.conceptMap.item(3).backgroundColor, "#ffeeaa");
assert.equal(model.toDocument().concepts.length, 1);
});
test("semantic relations and ownership remain independent of map connectors", () => {
const repository = new ConceptRepository([
{ id: firstId, label: "Parent" },
{ id: secondId, label: "Child" }
]);
repository.addRelation(new ConceptRelation("contains", firstId, secondId, {
label: "contains"
}));
repository.addOwnership(new ConceptOwnership(firstId, secondId));
const conceptMap = new ConceptMap();
conceptMap.addItem(new ConceptMapConcept(1, firstId, { x: 0, y: 0 }));
conceptMap.addItem(new ConceptMapConcept(2, secondId, {
parentSubmapId: 1, x: 50, y: 80
}));
conceptMap.addConnector(new ConceptMapConnector(1, 1, 2, {
relationId: "contains"
}));
const document = new CmapModel(repository, conceptMap).toDocument();
assert.equal(document.conceptRelations[0].id, "contains");
assert.equal(document.conceptOwnerships[0].parentConceptId, firstId);
assert.equal(document.items[1].parentSubmapId, 1);
assert.equal(document.connectors[0].relationId, "contains");
});
test("concept ownership rejects cycles", () => {
const repository = new ConceptRepository([
{ id: firstId, label: "First" },
{ id: secondId, label: "Second" }
]);
repository.addOwnership({ parentConceptId: firstId, childConceptId: secondId });
assert.throws(() => repository.addOwnership({
parentConceptId: secondId,
childConceptId: firstId
}), /cycle/);
});
test("an embedded submap becomes an independent map linked from its owner", () => {
const document = {
schemaVersion: 2,
metadata: { tags: [], summary: "Parent", explanationPageSlug: "" },
concepts: [
{ id: firstId, label: "Owner", synopsis: "Nested subject", aspects: ["design"] },
{ id: secondId, label: "Nested concept" },
{ id: thirdId, label: "Outside concept" }
],
conceptOwnerships: [
{ parentConceptId: firstId, childConceptId: secondId },
{ parentConceptId: firstId, childConceptId: thirdId }
],
items: [
{ id: 1, conceptId: firstId, kind: "submap", x: 100, y: 100, submapDepth: 0 },
{ id: 2, conceptId: secondId, kind: "concept", x: 240, y: 260,
parentSubmapId: 1, submapDepth: 1 },
{ id: 3, kind: "phrase", label: "contains", x: 450, y: 270,
parentSubmapId: 1, submapDepth: 1 },
{ id: 4, conceptId: thirdId, kind: "concept", x: 20, y: 100, submapDepth: 0 }
],
connectors: [
{ id: 1, sourceId: 2, targetId: 3 },
{ id: 2, sourceId: 4, targetId: 2 },
{ id: 3, sourceId: 1, targetId: 2 }
],
conceptMaps: []
};
const extraction = CmapModel.fromDocument(document)
.extractSubmap(1, "nested-subject");
assert.deepEqual(extraction.parentDocument.items.map((item) => item.id), [1, 4]);
assert.equal(extraction.parentDocument.items[0].kind, "concept");
assert.equal(extraction.parentDocument.concepts
.find((concept) => concept.id === firstId).cmapSlug, "nested-subject");
assert.deepEqual(extraction.parentDocument.connectors.map((connector) => ({
sourceId: connector.sourceId,
targetId: connector.targetId
})), [{ sourceId: 4, targetId: 1 }]);
assert.deepEqual(extraction.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");
const convertedDerivedMap = CmapModel.fromDocument(document).extractSubmap(
1,
"nested-subject",
{ tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: "" }
);
assert.deepEqual(convertedDerivedMap.childDocument.metadata, {
tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: ""
});
});
test("a model document is accepted by the complete JSON bundle export", async () => {
const model = CmapModel.fromDocument(sampleDocument());
const bundle = await buildBundle({
rootMap: {
slug: "architecture",
title: "Architecture",
document: model.toDocument()
},
maxDepth: 0,
exportedAt: "2026-09-01T12:00:00.000Z",
loadConceptMap: async () => { throw new Error("not requested"); },
loadWikiPage: async (reference) => ({
slug: reference,
title: reference,
markdown: "Description",
tags: []
}),
loadAttachment: async () => { throw new Error("not requested"); }
});
assert.equal(bundle.rootCmapSlug, "architecture");
assert.deepEqual(bundle.cmaps[0].document.items, model.toDocument().items);
assert.equal(bundle.concepts[0].label, "Application");
});