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
+4 -4
View File
@@ -74,7 +74,7 @@ function fakeLink(attributes) {
};
}
function loadEditorFactory() {
async function loadEditorFactory() {
global.Element = class Element {};
global.document = {
currentScript: null,
@@ -88,12 +88,12 @@ function loadEditorFactory() {
clearTimeout,
requestAnimationFrame: (callback) => callback()
};
require("../static/cmap/cmap-racket-wiki.js");
await import("../static/cmap/cmap-racket-wiki.js");
return global.window.RacketWikiCmap;
}
test("a dragged connector endpoint survives collapsing and expanding its submap", () => {
const factory = loadEditorFactory();
test("a dragged connector endpoint survives collapsing and expanding its submap", async () => {
const factory = await loadEditorFactory();
const links = [];
const map = {
onSelection() {},
+4 -4
View File
@@ -21,7 +21,7 @@ function fakeElement({ width = 320, height = 120 } = {}) {
};
}
function loadEditorFactory() {
async function loadEditorFactory() {
global.Element = class Element {};
global.document = {
currentScript: null,
@@ -30,12 +30,12 @@ function loadEditorFactory() {
createElement: () => fakeElement()
};
global.window = { Cmap: null };
require("../static/cmap/cmap-racket-wiki.js");
await import("../static/cmap/cmap-racket-wiki.js");
return global.window.RacketWikiCmap;
}
test("automatic render sizing advances only an unchanged saved baseline", () => {
const factory = loadEditorFactory();
test("automatic render sizing advances only an unchanged saved baseline", async () => {
const factory = await loadEditorFactory();
const canvas = {
addEventListener() {},
querySelector: () => null,
+33 -3
View File
@@ -2,9 +2,21 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const {
buildBundle, validateBundle, preparedMapDocument, attachmentUrls, replaceAttachmentUrls
} = require("../static/js/cmap-interchange.js");
let buildBundle;
let validateBundle;
let preparedMapDocument;
let attachmentUrls;
let replaceAttachmentUrls;
test.before(async () => {
({
buildBundle,
validateBundle,
preparedMapDocument,
attachmentUrls,
replaceAttachmentUrls
} = await import("../static/js/wiki/cmap/interchange.js"));
});
const conceptId = "24d27086-9f8b-4b57-a7fb-47b513277555";
const childId = "53ef1cf3-c936-48f7-889a-ad644fab9321";
@@ -150,6 +162,24 @@ test("invalid connector endpoints and duplicate slugs are rejected", () => {
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",
+2 -1
View File
@@ -88,7 +88,8 @@ test("relations render and hit-test behind concept nodes", async () => {
requestAnimationFrame: (callback) => setImmediate(callback)
};
const Cmap = require("../static/cmap/cmap.js");
await import("../static/cmap/cmap.js");
const Cmap = global.window.Cmap;
const map = Cmap(root);
const concept = map.node({ x: 40, y: 0, width: 120, height: 50 });
const otherConcept = map.node({ x: 220, y: 0, width: 120, height: 50 });
+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");
});
+10 -4
View File
@@ -16,7 +16,7 @@ function fakeNode(attributes) {
};
}
function loadEditorFactory() {
async function loadEditorFactory() {
global.Element = class Element {};
global.document = {
currentScript: null,
@@ -30,12 +30,12 @@ function loadEditorFactory() {
clearTimeout,
requestAnimationFrame: (callback) => callback()
};
require("../static/cmap/cmap-racket-wiki.js");
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 = loadEditorFactory();
const factory = await loadEditorFactory();
const map = {
onSelection() {},
onActivation() {},
@@ -53,6 +53,12 @@ test("the last selected concept determines target size and alignment", async ()
});
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,
@@ -141,7 +147,7 @@ test("the last selected concept determines target size and alignment", async ()
[Number(item.node.attr("width")), Number(item.node.attr("height"))]),
[[120, 45], [120, 45], [120, 45]]);
await new Promise((resolve) => setImmediate(resolve));
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]]);
+4 -4
View File
@@ -16,7 +16,7 @@ function fakeNode(attributes) {
};
}
function loadEditorFactory() {
async function loadEditorFactory() {
global.Element = class Element {};
global.document = {
currentScript: null,
@@ -30,12 +30,12 @@ function loadEditorFactory() {
clearTimeout,
requestAnimationFrame: (callback) => callback()
};
require("../static/cmap/cmap-racket-wiki.js");
await import("../static/cmap/cmap-racket-wiki.js");
return global.window.RacketWikiCmap;
}
test("back from a second nested submap returns exactly one map level", () => {
const factory = loadEditorFactory();
test("back from a second nested submap returns exactly one map level", async () => {
const factory = await loadEditorFactory();
const map = {
onSelection() {},
onActivation() {},
+79
View File
@@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
import { BreadcrumbTrail } from "../static/js/wiki/breadcrumb-trail.js";
class MemoryStorage {
constructor() {
this.values = new Map();
}
getItem(key) {
return this.values.has(key) ? this.values.get(key) : null;
}
setItem(key, value) {
this.values.set(key, String(value));
}
}
test("stored breadcrumbs are limited to current readable pages", () => {
const storage = new MemoryStorage();
storage.setItem("racket-wiki-breadcrumb-trail", JSON.stringify([
"first", "removed", 42, "second"
]));
const trail = new BreadcrumbTrail(storage);
assert.deepEqual(trail.load(["first", "second"]), ["first", "second"]);
assert.equal(
storage.getItem("racket-wiki-breadcrumb-trail"),
JSON.stringify(["first", "second"]));
});
test("invalid stored JSON starts with an empty trail", () => {
const storage = new MemoryStorage();
storage.setItem("racket-wiki-breadcrumb-trail", "not JSON");
const trail = new BreadcrumbTrail(storage);
assert.deepEqual(trail.load(["first"]), []);
});
test("page visits append, truncate revisits and clear at home", () => {
const storage = new MemoryStorage();
const trail = new BreadcrumbTrail(storage);
trail.load(["home", "first", "second", "third"]);
trail.record("first", "home");
trail.record("second", "home");
trail.record("third", "home");
assert.deepEqual(trail.slugs, ["first", "second", "third"]);
assert.deepEqual(trail.record("second", "home"), ["first", "second"]);
const returnedSlugs = trail.slugs;
returnedSlugs.push("outside-change");
assert.deepEqual(trail.slugs, ["first", "second"]);
assert.deepEqual(trail.record("home", "home"), []);
});
test("the trail retains at most eight non-home pages", () => {
const storage = new MemoryStorage();
const trail = new BreadcrumbTrail(storage);
const pages = Array.from({ length: 10 }, (_value, index) => `page-${index}`);
trail.load(["home", ...pages]);
for (const page of pages) trail.record(page, "home");
assert.deepEqual(trail.slugs, pages.slice(-8));
});
test("selecting a breadcrumb discards only its successors", () => {
const storage = new MemoryStorage();
const trail = new BreadcrumbTrail(storage);
trail.load(["home", "first", "second", "third"]);
trail.record("first", "home");
trail.record("second", "home");
trail.record("third", "home");
assert.deepEqual(trail.truncate("first", "home"), ["first"]);
assert.deepEqual(trail.truncate("missing", "home"), ["first"]);
});
+69
View File
@@ -0,0 +1,69 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
applyImageWidthMarkup,
cmapMentionTarget,
escapeHtml,
expandNamespacedMarkdownLinks,
expandTodoMarkup,
expandWikiMentions,
extractCmapEmbeds,
restoreCmapEmbeds
} from "../static/js/wiki/markdown.js";
test("Todo markup is escaped, numbered and left alone in fenced code", () => {
const markdown = "todo(<script>)\n```\ntodo(no change)\n```\ntodo(second)";
const expanded = expandTodoMarkup(markdown, "start", "Todo & note");
assert.match(expanded, /#todo\/start\/1/);
assert.match(expanded, /#todo\/start\/2/);
assert.match(expanded, /Todo &amp; note/);
assert.match(expanded, /&lt;script&gt;/);
assert.match(expanded, /```\ntodo\(no change\)\n```/);
});
test("image options add width and alignment to rendered image tags", () => {
assert.equal(
applyImageWidthMarkup('<img src="x.png"> { width=40% center }'),
'<img src="x.png" style="width: 40%" class="wiki-image-center">');
assert.equal(
applyImageWidthMarkup('<img class="existing" src="x.png"> { width=240 left float }'),
'<img class="existing wiki-image-float-left" src="x.png" style="width: 240px">');
});
test("explicit namespaced links become internal routes outside code", () => {
const markdown = "[Page](notes:roadmap)\n[CMap](cmap:architecture)\n```\n[Code](notes:keep)\n```";
assert.equal(
expandNamespacedMarkdownLinks(markdown),
"[Page](#/notes%3Aroadmap)\n[CMap](#cmap/architecture)\n```\n[Code](notes:keep)\n```");
});
test("WikiWords use page and CMap catalogues without expanding protected text", () => {
const pages = [
{ slug: "homeopathie", namespace: "", pageSlug: "homeopathie", title: "Homeopathie Wiki" }
];
const conceptMaps = [{ slug: "architectuur", title: "Architectuur Kaart" }];
const markdown = "HomeopathieWiki en cmap:ArchitectuurKaart, maar `NieuwePagina` niet.";
assert.equal(
expandWikiMentions(markdown, pages, [], conceptMaps),
"[Homeopathie Wiki](#/homeopathie) en [Architectuur Kaart](#cmap/architectuur), maar `NieuwePagina` niet.");
});
test("ambiguous CMap mentions are not resolved", () => {
const conceptMaps = [
{ slug: "first", title: "Zelfde Kaart" },
{ slug: "second", title: "ZelfdeKaart" }
];
assert.equal(cmapMentionTarget("ZelfdeKaart", conceptMaps), null);
});
test("CMap embeds survive Markdown rendering through escaped placeholders", () => {
const extracted = extractCmapEmbeds("before\n{{cmap:architecture}}\n```\n{{cmap:code}}\n```");
assert.equal(extracted.embeds.length, 1);
assert.match(extracted.markdown, /RACKETWIKICMAPEMBED0TOKEN/);
assert.match(extracted.markdown, /\{\{cmap:code\}\}/);
assert.equal(
restoreCmapEmbeds("<p>RACKETWIKICMAPEMBED0TOKEN</p>", extracted.embeds, "Loading…"),
'<section class="rw-cmap-embed" data-cmap-reference="architecture"><div class="rw-cmap-embed-loading">Loading…</div></section>');
assert.equal(escapeHtml('<a href="x">&'), "&lt;a href=&quot;x&quot;&gt;&amp;");
});
+30
View File
@@ -0,0 +1,30 @@
import test from "node:test";
import assert from "node:assert/strict";
import { headingId, markdownHeadings } from "../static/js/wiki/page-outline.js";
test("heading identifiers are readable and unique", () => {
const usedIds = new Set();
assert.equal(headingId("Résumé & planning", usedIds), "resume-planning");
assert.equal(headingId("Résumé & planning", usedIds), "resume-planning-2");
assert.equal(headingId("***", usedIds), "section");
});
test("Markdown headings retain their source line and level", () => {
const markdown = "intro\n# First\ntext\n ### Third ###\n";
assert.deepEqual(markdownHeadings(markdown), [
{ level: 1, text: "First", line: 1 },
{ level: 3, text: "Third", line: 3 }
]);
});
test("headings inside fenced code are ignored", () => {
const markdown = "# Visible\n```racket\n## Code\n```\n## Also visible";
assert.deepEqual(markdownHeadings(markdown), [
{ level: 1, text: "Visible", line: 0 },
{ level: 2, text: "Also visible", line: 4 }
]);
});
+43
View File
@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
newPageReference,
pageReference,
slugTitle,
splitPageReference,
wikiWordTarget
} from "../static/js/wiki/reference.js";
test("wiki references preserve root and namespace syntax", () => {
assert.deepEqual(splitPageReference("roadmap"), { namespace: "", slug: "roadmap" });
assert.deepEqual(splitPageReference("racket:roadmap"), {
namespace: "racket",
slug: "roadmap"
});
assert.equal(pageReference("", "roadmap"), "roadmap");
assert.equal(pageReference(" racket ", "roadmap"), "racket:roadmap");
});
test("freely entered page names become bounded wiki references", () => {
assert.equal(newPageReference("Racket:Résumé pagina"), "racket:resume-pagina");
assert.equal(newPageReference(" Nieuwe pagina "), "nieuwe-pagina");
assert.equal(newPageReference(""), null);
assert.equal(newPageReference(":"), null);
});
test("WikiWords resolve to existing aliases or predictable new targets", () => {
const aliases = new Map([
[":homeopathiewiki", { slug: "homeopathie", title: "Homeopathie Wiki" }]
]);
assert.deepEqual(wikiWordTarget("HomeopathieWiki", aliases), {
slug: "homeopathie",
title: "Homeopathie Wiki"
});
assert.deepEqual(wikiWordTarget("NieuwePagina", aliases, "notes"), {
slug: "notes:nieuwe-pagina",
title: "Nieuwe Pagina"
});
assert.equal(wikiWordTarget("geen-wikiword", aliases), null);
assert.equal(slugTitle("notes:nieuwe-pagina"), "Nieuwe Pagina");
});
+60
View File
@@ -0,0 +1,60 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
cmapRoute,
pageRoute,
parseWikiRoute
} from "../static/js/wiki/routes.js";
test("route builders encode references without changing their identity", () => {
assert.equal(pageRoute("notes:een pagina"), "#/notes%3Aeen%20pagina");
assert.equal(cmapRoute("een kaart"), "#cmap/een%20kaart");
});
test("page, CMap and Todo routes decode their parameters", () => {
assert.deepEqual(parseWikiRoute("#/notes%3Aeen%20pagina"), {
name: "page",
slug: "notes:een pagina"
});
assert.deepEqual(parseWikiRoute("#cmap/een%20kaart"), {
name: "cmap",
slug: "een kaart"
});
assert.deepEqual(parseWikiRoute("#todo/notes%3Aplanning/12"), {
name: "todo",
slug: "notes:planning",
number: 12
});
});
test("search and fixed special routes have explicit names", () => {
assert.deepEqual(parseWikiRoute("#search/een%20term"), {
name: "search",
query: "een term"
});
assert.deepEqual(parseWikiRoute("#profile"), { name: "profile" });
assert.deepEqual(parseWikiRoute("#cmaps"), { name: "cmaps" });
assert.deepEqual(parseWikiRoute("#recent"), { name: "recent" });
assert.deepEqual(parseWikiRoute("#bookmarks"), { name: "bookmarks" });
assert.deepEqual(parseWikiRoute("#todos"), { name: "todos" });
});
test("administration routes remain distinct for permission dispatch", () => {
assert.deepEqual(parseWikiRoute("#admin"), { name: "admin" });
assert.deepEqual(parseWikiRoute("#admin/users"), { name: "admin-users" });
assert.deepEqual(parseWikiRoute("#admin/mail"), { name: "admin-mail" });
assert.deepEqual(parseWikiRoute("#admin/aliases"), { name: "admin-aliases" });
assert.deepEqual(parseWikiRoute("#admin/orphaned-uploads"), {
name: "admin-orphaned-uploads"
});
assert.deepEqual(parseWikiRoute("#admin/archived-cmaps"), {
name: "admin-archived-cmaps"
});
});
test("empty, incomplete and unknown hashes fall back to home", () => {
assert.deepEqual(parseWikiRoute(""), { name: "home" });
assert.deepEqual(parseWikiRoute("#search/"), { name: "home" });
assert.deepEqual(parseWikiRoute("#unknown"), { name: "home" });
});