1// @vitest-environment jsdom
2
3import { act, useLayoutEffect } from "react";
4import { createRoot, type Root } from "react-dom/client";
5import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
6import { ChatCanvas } from "./ChatCanvas";
7import { useChatCanvas } from "./ChatCanvasContext";
8
9const resize = vi.hoisted(() => ({ callbacks: new Set<() => void>() }));
10vi.mock("../../lib/observeResize", () => ({
11 observeResize: (_elements: unknown, callback: () => void) => {
12 resize.callbacks.add(callback);
13 return () => resize.callbacks.delete(callback);
14 },
15}));
16
17let root: Root;
18let container: HTMLDivElement;
19let fontSize = 16;
20let canvas: ReturnType<typeof useChatCanvas>;
21
22function Preview({ x }: { x: number }) {
23 const current = useChatCanvas();
24 useLayoutEffect(() => {
25 canvas = current;
26 }, [current]);
27 const reportPreview = current?.reportPreview;
28 useLayoutEffect(() => {
29 reportPreview?.({
30 key: "browser:one",
31 width: 240,
32 source: { width: 240, height: 365 },
33 position: { x, y: 12 },
34 lastInteraction: "drag",
35 });
36 }, [reportPreview, x]);
37 return null;
38}
39
40beforeEach(() => {
41 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
42 fontSize = 16;
43 vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(1800);
44 vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(1000);
45 vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (
46 this: HTMLElement,
47 ) {
48 // A nonzero canvas origin catches viewport coordinates used as canvas coordinates.
49 if (this.hasAttribute("data-thread-find-bar")) {
50 const gap = 0.75 * fontSize;
51 return new DOMRect(
52 200 + 1800 - gap - 17.5 * fontSize,
53 100 + gap,
54 17.5 * fontSize,
55 2.25 * fontSize,
56 );
57 }
58 return new DOMRect(200, 100, 1800, 1000);
59 });
60 vi.spyOn(window, "getComputedStyle").mockReturnValue({
61 paddingLeft: "48px",
62 width: "736px",
63 minWidth: "640px",
64 } as CSSStyleDeclaration);
65 container = document.createElement("div");
66 document.body.append(container);
67 root = createRoot(container);
68});
69
70afterEach(async () => {
71 await act(() => root.unmount());
72 container.remove();
73 resize.callbacks.clear();
74 vi.restoreAllMocks();
75 vi.unstubAllGlobals();
76});
77
78async function renderFind(open: boolean, previewX = 1548) {
79 await act(() =>
80 root.render(
81 <ChatCanvas composerOverlayElement={null} detailsCardTopInset={open ? 48 : 0}>
82 {open ? <div data-thread-find-bar /> : null}
83 <Preview x={previewX} />
84 </ChatCanvas>,
85 ),
86 );
87}
88
89it.each([16, 24])(
90 "keeps a preview below Find and releases its space on close at a %spx root font",
91 async (size) => {
92 fontSize = size;
93 await renderFind(false);
94 expect(canvas?.layout.frame?.y).toBe(12);
95
96 await renderFind(true);
97 expect(canvas?.detailsCardTopInset).toBe(3 * size);
98 expect(canvas?.layout.frame?.y).toBe(3 * size + 12);
99
100 await renderFind(false);
101 expect(canvas?.detailsCardTopInset).toBe(0);
102 expect(canvas?.layout.frame?.y).toBe(12);
103
104 await renderFind(true);
105 expect(canvas?.layout.frame?.y).toBe(3 * size + 12);
106 },
107);
108
109it("remeasures the open bar when its font size changes without resizing the canvas", async () => {
110 await renderFind(true, 1200);
111 expect(canvas?.layout.frame?.y).toBe(12);
112
113 fontSize = 24;
114 await act(() => {
115 for (const callback of resize.callbacks) callback();
116 });
117 expect(canvas?.detailsCardTopInset).toBe(72);
118 expect(canvas?.layout.frame?.y).toBe(84);
119});