test.build/core/converters/learning.catalog.converter.test.ts
test.build/core/converters/learning.catalog.converter.test.ts is a file in Codebase Testing. 116 lines of code and 0 definitions.
import type { Identity, NumberRow } from "@banes-lab/build-scripts/types/catalog.types.ts";
import { describe, expect, it } from "vitest";
import {
navigationOf,
numberRows,
numbersLeaves,
routeLeaf,
routeStops,
} from "@banes-lab/build-scripts/core/converters/learning.catalog.converter.ts";
import type { LearningBlock } from "@banes-lab/web/types/learning.types.js";
import { createLinker } from "@banes-lab/build-scripts/core/resolvers/link.resolver.ts";
import { graphNode } from "./graph.fixture.ts";
const SITE = "https://example.test";
const ONE = "chapter:/method#one";
const BLOCKS: readonly LearningBlock[] = [
{
code: "aa",
label: "Start",
owner: "method",
stops: [
{ id: "aa1", label: "01 - One", path: "/method#one", requires: [] },
{ id: "aa2", label: "02 - Two", path: "/method#two", requires: ["aa1"] },
],
},
{
code: "ab",
label: "Next",
owner: "method",
stops: [{ id: "ab3", label: "03 - Three", path: "/method/next#three", requires: ["aa1", "aa2"] }],
},
];
const identityOf = function identityOf(path: string, title: string): Identity {
const [page = "", section = ""] = path.split("#");
return {
address: { json: `/json${page}/${section}`, markdown: `${page}/${section}.md` },
href: path,
kind: "section",
ref: `chapter:${path}`,
summary: null,
title,
};
};
const LINKER = createLinker(
[identityOf("/method#one", "One"), identityOf("/method#two", "Two"), identityOf("/method/next#three", "Three")],
SITE,
(href) => href,
);
describe("routeStops", () => {
it("numbers every stop across the blocks in teaching order", () => {
expect(routeStops(BLOCKS).map((stop) => [stop.id, stop.position, stop.block])).toStrictEqual([
["aa1", 1, "Start"],
["aa2", 2, "Start"],
["ab3", 3, "Next"],
]);
});
});
describe("navigationOf", () => {
it("gives a section its place, its neighbors and the stops it builds on", () => {
const navigate = navigationOf(routeStops(BLOCKS), LINKER);
const middle = navigate("/method#two");
expect(middle?.position).toBe(2);
expect(middle?.total).toBe(3);
expect(middle?.previous?.ref).toBe(ONE);
expect(middle?.next?.ref).toBe("chapter:/method/next#three");
expect(middle?.requires.map((link) => link.ref)).toStrictEqual([ONE]);
expect(navigate("/method#one")?.previous).toBeNull();
expect(navigate("/method/next#three")?.next).toBeNull();
expect(navigate("/elsewhere#x")).toBeNull();
});
});
const numberRow = function numberRow(index: number): NumberRow {
return { kind: "section", link: LINKER.link("One", ONE), number: String(index), part: null };
};
describe("numberRows", () => {
it("numbers each site section by the graph's number and each captioned part within its section", () => {
const section = { ...graphNode(ONE, "/method#one"), kind: "section", number: "12" };
const part = { ...graphNode("part:/method#one/1", null), kind: "part", number: "12.1" };
const record = { ...graphNode("architecture:x", null), layer: "ontology" as const, number: "9" };
const graph = {
edges: [{ from: ONE, relation: "contains", to: "part:/method#one/1" }],
nodes: [section, part, record],
};
const rows = numberRows(graph, [], LINKER);
expect(rows.map((row) => [row.number, row.kind, row.part, row.link.ref])).toStrictEqual([
["12", "section", null, ONE],
["12.1", "part", 1, ONE],
]);
});
});
describe("numbersLeaves", () => {
it("publishes the numbers whole while they fit, and as parts under a head that names them when they do not", () => {
const rows = Array.from({ length: 40 }, (_, index) => numberRow(index + 1));
const [whole] = numbersLeaves(rows, SITE, "Numbers");
expect(whole?.identity.address.json).toBe("/json/api/numbers");
expect(Reflect.get(whole?.data ?? {}, "total")).toBe(40);
const split = numbersLeaves(rows, SITE, "Numbers", 2000, 1000);
const head = split.at(-1);
const parts = split.slice(0, -1);
expect(parts.length).toBeGreaterThan(1);
expect(parts[0]?.identity.address.json).toBe("/json/api/numbers/_1");
expect(parts.every((leaf) => leaf.identity.kind === "index-part")).toBe(true);
expect(head?.identity.address.json).toBe("/json/api/numbers");
const listed: unknown = Reflect.get(head?.data ?? {}, "parts");
expect(Array.isArray(listed) ? listed.length : 0).toBe(parts.length);
expect(head?.markdown).toContain("/api/numbers/_1.md");
});
});
describe("routeLeaf", () => {
it("publishes the route as data and as a numbered Markdown list", () => {
const leaf = routeLeaf(routeStops(BLOCKS), LINKER, "Learning route");
expect(leaf.identity.address.json).toBe("/json/api/route");
expect(leaf.markdown).toContain(
"3. [03 - Three](https://example.test/method/next/three.md) (Next, after stop 1, 2)",
);
});
});