Skip to content
File

Blob: tests/client/components/editor/controllers/menu/popover.dom.test.tsx

typescript101 lines
1import { act } from "react";
2import { createRoot, type Root } from "react-dom/client";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4 
5let latestPositionUpdate: (() => void | Promise<void>) | null = null;
6 
7beforeEach(() => {
8 vi.resetModules();
9 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
10 latestPositionUpdate = null;
11});
12 
13afterEach(() => {
14 vi.restoreAllMocks();
15 vi.unstubAllGlobals();
16 document.body.innerHTML = "";
17});
18 
19async function flushFloatingPosition() {
20 await act(async () => {
21 await latestPositionUpdate?.();
22 await Promise.resolve();
23 await Promise.resolve();
24 });
25}
26 
27describe("useEditorRectPopover", () => {
28 it("hides while closed or unresolved instead of reusing stale coordinates", async () => {
29 vi.doMock("@floating-ui/dom", () => ({
30 autoUpdate: vi.fn((_reference: Element, _floating: HTMLElement, update: () => void | Promise<void>) => {
31 latestPositionUpdate = update;
32 void update();
33 return () => {
34 if (latestPositionUpdate === update) {
35 latestPositionUpdate = null;
36 }
37 };
38 }),
39 computePosition: vi.fn(async (reference: { getBoundingClientRect: () => DOMRect }) => {
40 const rect = reference.getBoundingClientRect();
41 return { x: rect.left + 10, y: rect.top + 20 };
42 }),
43 flip: vi.fn(() => ({})),
44 offset: vi.fn(() => ({})),
45 shift: vi.fn(() => ({})),
46 size: vi.fn(() => ({})),
47 }));
48 
49 const { useEditorRectPopover } = await import("@/client/components/editor/controllers/menu/popover");
50 
51 function Probe({ open, rect }: { open: boolean; rect: DOMRect | null }) {
52 const { floatingStyles, setFloating } = useEditorRectPopover({
53 open,
54 getAnchorRect: () => rect,
55 contextElement: () => document.body,
56 });
57 return <div data-testid="floating" ref={setFloating} style={floatingStyles} />;
58 }
59 
60 const host = document.createElement("div");
61 document.body.appendChild(host);
62 let root: Root | null = createRoot(host);
63 
64 const floating = () => host.querySelector<HTMLElement>('[data-testid="floating"]');
65 const render = async (open: boolean, rect: DOMRect | null) => {
66 await act(async () => {
67 root!.render(<Probe open={open} rect={rect} />);
68 });
69 await flushFloatingPosition();
70 };
71 
72 try {
73 await render(true, new DOMRect(100, 200, 10, 10));
74 expect(floating()?.style.left).toBe("110px");
75 expect(floating()?.style.top).toBe("220px");
76 expect(floating()?.style.visibility).toBe("");
77 
78 await render(false, new DOMRect(100, 200, 10, 10));
79 expect(floating()?.style.left).toBe("0px");
80 expect(floating()?.style.top).toBe("0px");
81 expect(floating()?.style.visibility).toBe("hidden");
82 
83 await render(true, null);
84 expect(floating()?.style.left).toBe("0px");
85 expect(floating()?.style.top).toBe("0px");
86 expect(floating()?.style.visibility).toBe("hidden");
87 
88 await render(true, new DOMRect(5, 6, 1, 1));
89 expect(floating()?.style.left).toBe("15px");
90 expect(floating()?.style.top).toBe("26px");
91 expect(floating()?.style.visibility).toBe("");
92 } finally {
93 await act(async () => {
94 root?.unmount();
95 });
96 root = null;
97 host.remove();
98 }
99 });
100});