Skip to content
File

Blob: tests/client/sites/islands/site-outline-controller.dom.test.tsx

typescript84 lines
1import { act } from "react";
2import { createRoot, type Root } from "react-dom/client";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4 
5import { SiteOutlineController } from "@/client/sites/islands/site-outline-controller";
6 
7let host: HTMLElement;
8let root: Root;
9 
10beforeEach(() => {
11 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
12 vi.useFakeTimers();
13 vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
14 return window.setTimeout(() => cb(performance.now()), 0) as unknown as number;
15 });
16 vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) => {
17 window.clearTimeout(id);
18 });
19 
20 document.body.innerHTML = `
21 <h2 id="intro">Intro</h2>
22 <h2 id="second">Second</h2>
23 <nav class="tiptap-outline">
24 <a class="tiptap-outline__button tiptap-outline__link" data-outline-id="intro" data-active="false" href="#intro">Intro</a>
25 <a class="tiptap-outline__button tiptap-outline__link" data-outline-id="second" data-active="false" href="#second">Second</a>
26 </nav>
27 <div id="rail-host"></div>
28 `;
29 
30 const intro = document.getElementById("intro")!;
31 const second = document.getElementById("second")!;
32 Object.defineProperty(intro, "offsetParent", { configurable: true, get: () => document.body });
33 Object.defineProperty(second, "offsetParent", { configurable: true, get: () => document.body });
34 intro.getBoundingClientRect = () => ({ top: 40, bottom: 80 }) as DOMRect;
35 second.getBoundingClientRect = () => ({ top: 520, bottom: 560 }) as DOMRect;
36 
37 host = document.getElementById("rail-host")!;
38 root = createRoot(host);
39 act(() => {
40 root.render(
41 <SiteOutlineController
42 items={[
43 { id: "intro", text: "Intro", level: 1, href: "#intro" },
44 { id: "second", text: "Second", level: 1, href: "#second" },
45 ]}
46 />,
47 );
48 });
49});
50 
51afterEach(() => {
52 act(() => {
53 root.unmount();
54 });
55 document.body.innerHTML = "";
56 vi.useRealTimers();
57 vi.restoreAllMocks();
58 vi.unstubAllGlobals();
59});
60 
61describe("SiteOutlineController", () => {
62 it("updates every duplicate outline link on simulated viewport changes", () => {
63 expect(document.querySelectorAll("[data-outline-id='intro'][data-active='true']").length).toBeGreaterThanOrEqual(2);
64 expect(
65 document.querySelectorAll("[data-outline-id='intro'][aria-current='location']").length,
66 ).toBeGreaterThanOrEqual(2);
67 
68 const intro = document.getElementById("intro")!;
69 const second = document.getElementById("second")!;
70 intro.getBoundingClientRect = () => ({ top: -140, bottom: -80 }) as DOMRect;
71 second.getBoundingClientRect = () => ({ top: 180, bottom: 220 }) as DOMRect;
72 
73 act(() => {
74 window.dispatchEvent(new Event("scroll"));
75 vi.runOnlyPendingTimers();
76 });
77 
78 expect(document.querySelectorAll("[data-outline-id='second'][data-active='true']").length).toBeGreaterThanOrEqual(
79 2,
80 );
81 expect(document.querySelectorAll("[data-outline-id='intro'][aria-current='location']")).toHaveLength(0);
82 });
83});