| 1 | // @vitest-environment jsdom |
| 2 | |
| 3 | import { act, useLayoutEffect } from "react"; |
| 4 | import { createRoot, type Root } from "react-dom/client"; |
| 5 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 6 | import { ChatCanvas } from "./ChatCanvas"; |
| 7 | import { useChatCanvas } from "./ChatCanvasContext"; |
| 8 | |
| 9 | const resize = vi.hoisted(() => ({ callbacks: new Set<() => void>() })); |
| 10 | vi.mock("../../lib/observeResize", () => ({ |
| 11 | observeResize: (_elements: unknown, callback: () => void) => { |
| 12 | resize.callbacks.add(callback); |
| 13 | return () => resize.callbacks.delete(callback); |
| 14 | }, |
| 15 | })); |
| 16 | |
| 17 | let root: Root; |
| 18 | let container: HTMLDivElement; |
| 19 | let fontSize = 16; |
| 20 | let canvas: ReturnType<typeof useChatCanvas>; |
| 21 | |
| 22 | function 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 | |
| 40 | beforeEach(() => { |
| 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 | |
| 70 | afterEach(async () => { |
| 71 | await act(() => root.unmount()); |
| 72 | container.remove(); |
| 73 | resize.callbacks.clear(); |
| 74 | vi.restoreAllMocks(); |
| 75 | vi.unstubAllGlobals(); |
| 76 | }); |
| 77 | |
| 78 | async 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 | |
| 89 | it.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 | |
| 109 | it("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 | }); |