1import { describe, expect, it } from "vitest";
2import { anchorHref, generateToc, headingSlug, type TocBlockInput } from "./toc.js";
3
4const b = (id: string, type: string, text: string, parentId: string | null = null, position = "a0"): TocBlockInput => ({
5 id,
6 type,
7 content: { text },
8 parentId,
9 position
10});
11
12describe("TOC + heading anchors (T-147)", () => {
13 it("generates entries in document order with levels 1-3", () => {
14 const blocks: TocBlockInput[] = [
15 b("p1", "PARAGRAPH", "intro"),
16 b("h1", "HEADING_1", "Alpha"),
17 b("h2", "HEADING_2", "Beta"),
18 b("h3", "HEADING_3", "Gamma"),
19 b("h4", "HEADING_4" as string, "nope"),
20 b("p2", "PARAGRAPH", "more")
21 ];
22 const toc = generateToc(blocks);
23 expect(toc.map((e) => [e.level, e.text])).toEqual([
24 [1, "Alpha"],
25 [2, "Beta"],
26 [3, "Gamma"]
27 ]);
28 expect(toc.every((e) => e.blockId)).toBe(true);
29 });
30
31 it("includes headings nested in the tree (toggle children) at their own level", () => {
32 const blocks: TocBlockInput[] = [
33 b("t1", "TOGGLE", "section", null, "a0"),
34 b("h2", "HEADING_2", "Nested", "t1", "a0"),
35 b("h1", "HEADING_1", "Top", null, "a1")
36 ];
37 const toc = generateToc(blocks);
38 expect(toc.map((e) => e.text)).toEqual(["Nested", "Top"]);
39 });
40
41 it("derives stable, de-duplicated slugs", () => {
42 expect(headingSlug("Hello, World!")).toBe("hello-world");
43 expect(headingSlug(" Mixed Case ")).toBe("mixed-case");
44 expect(headingSlug("")).toBe("");
45
46 const blocks: TocBlockInput[] = [b("a", "HEADING_1", "Intro", null, "a0"), b("c", "HEADING_1", "Intro", null, "a1")];
47 const toc = generateToc(blocks);
48 expect(toc.map((e) => e.anchor)).toEqual(["intro", "intro-2"]);
49 });
50
51 it("treats blocks with a missing parent as root (defensive)", () => {
52 const blocks: TocBlockInput[] = [b("x", "HEADING_1", "Orphan", "ghost", "a0")];
53 expect(generateToc(blocks).map((e) => e.text)).toEqual(["Orphan"]);
54 });
55
56 it("anchorHref produces a # fragment", () => {
57 expect(anchorHref("hello-world")).toBe("#hello-world");
58 });
59});
60