Skip to content
File

Blob: tests/shared/outline-rendering.test.ts

typescript98 lines
1import { createElement } from "react";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vitest";
4import {
5 createUniqueOutlineAnchorId,
6 normalizeOutlineText,
7 OutlinePresentation,
8 readOutlineLevel,
9 resolveViewportActiveOutlineHeading,
10 type OutlineItem,
11} from "@/shared/editor/presentation";
12 
13describe("shared outline rendering", () => {
14 it("normalizes outline text, heading levels, and duplicate-safe anchor ids", () => {
15 const used = new Set<string>();
16 
17 expect(normalizeOutlineText(" Intro\n\tsection ")).toBe("Intro section");
18 expect(readOutlineLevel(2)).toBe(2);
19 expect(readOutlineLevel("3")).toBe(3);
20 expect(readOutlineLevel(9)).toBe(1);
21 
22 expect(createUniqueOutlineAnchorId("Intro!", used)).toBe("intro");
23 expect(createUniqueOutlineAnchorId("Intro", used)).toBe("intro-2");
24 expect(createUniqueOutlineAnchorId("你好", used)).toBe("section");
25 expect(createUniqueOutlineAnchorId("?!", used)).toBe("section-2");
26 });
27 
28 it("renders matching button and link outline markup", () => {
29 const items: OutlineItem[] = [
30 { id: "intro", text: "Intro", level: 1, href: "#intro" },
31 { id: "deep", text: "Deep section", level: 3, href: "#custom-deep" },
32 ];
33 
34 const buttonHtml = renderToStaticMarkup(
35 createElement(OutlinePresentation, {
36 items,
37 activeId: "deep",
38 mode: "button",
39 title: "On this page",
40 }),
41 );
42 expect(buttonHtml).toContain('class="tiptap-outline"');
43 expect(buttonHtml).toContain('aria-label="On this page"');
44 expect(buttonHtml).toContain('<button type="button"');
45 expect(buttonHtml).toContain('data-outline-id="deep"');
46 expect(buttonHtml).toContain('data-active="true"');
47 expect(buttonHtml).toContain('aria-current="location"');
48 expect(buttonHtml).toContain("padding-inline-start:calc(0.5rem + 1.75rem)");
49 
50 const linkHtml = renderToStaticMarkup(
51 createElement(OutlinePresentation, {
52 items,
53 activeId: "intro",
54 mode: "link",
55 variant: "rail",
56 }),
57 );
58 expect(linkHtml).toContain('class="tiptap-outline tiptap-outline--rail"');
59 expect(linkHtml).toContain('<a class="tiptap-outline__button tiptap-outline__link"');
60 expect(linkHtml).toContain('href="#custom-deep"');
61 expect(linkHtml).toContain('data-outline-id="intro"');
62 expect(linkHtml).toContain('data-active="true"');
63 expect(linkHtml).toContain('aria-current="location"');
64 });
65 
66 it("uses the live editor viewport priority rules for active headings", () => {
67 expect(
68 resolveViewportActiveOutlineHeading(
69 [
70 { id: "intro", top: -12, bottom: 12 },
71 { id: "next", top: 220, bottom: 260 },
72 ],
73 { top: 0, height: 600 },
74 ),
75 ).toBe("intro");
76 
77 expect(
78 resolveViewportActiveOutlineHeading(
79 [
80 { id: "intro", top: -120, bottom: -20 },
81 { id: "next", top: 220, bottom: 260 },
82 ],
83 { top: 0, height: 600 },
84 ),
85 ).toBe("next");
86 
87 expect(
88 resolveViewportActiveOutlineHeading(
89 [
90 { id: "intro", top: -120, bottom: -20 },
91 { id: "next", top: 590, bottom: 640 },
92 ],
93 { top: 0, height: 600 },
94 ),
95 ).toBe("intro");
96 });
97});