1import { act, StrictMode, useLayoutEffect } from "react";
2import { createRoot, type Root } from "react-dom/client";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
4
5import { useComposerMenuState } from "./useComposerMenuState";
6
7let root: Root;
8let setOpen: ReturnType<typeof useComposerMenuState>[1];
9let committedOpen: boolean[];
10let closePopup = vi.fn<() => void>();
11
12function MenuStateProbe({ hidden }: { hidden: boolean }) {
13 const [open, updateOpen] = useComposerMenuState(hidden);
14 useLayoutEffect(() => {
15 setOpen = updateOpen;
16 committedOpen.push(open);
17 if (open) return closePopup;
18 }, [open, updateOpen]);
19 return null;
20}
21
22async function renderMenu(hidden: boolean) {
23 await act(() => {
24 root.render(
25 <StrictMode>
26 <MenuStateProbe hidden={hidden} />
27 </StrictMode>,
28 );
29 });
30}
31
32beforeEach(() => {
33 // The probe renders no host nodes, but ReactDOM still needs an event target.
34 const document = {
35 nodeType: 9,
36 addEventListener() {},
37 removeEventListener() {},
38 };
39 const container = {
40 nodeType: 1,
41 tagName: "DIV",
42 namespaceURI: "http://www.w3.org/1999/xhtml",
43 ownerDocument: document,
44 addEventListener() {},
45 removeEventListener() {},
46 };
47 vi.stubGlobal("document", document);
48 vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget });
49 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
50 committedOpen = [];
51 closePopup = vi.fn();
52 root = createRoot(container as unknown as HTMLElement);
53});
54
55afterEach(async () => {
56 await act(() => root.unmount());
57 vi.unstubAllGlobals();
58});
59
60describe("useComposerMenuState", () => {
61 it("closes an open popup when its trigger hides and does not reopen when it returns", async () => {
62 await renderMenu(false);
63 await act(() => setOpen(true));
64 expect(committedOpen.at(-1)).toBe(true);
65
66 await renderMenu(true);
67 expect(committedOpen.at(-1)).toBe(false);
68 expect(closePopup).toHaveBeenCalledTimes(1);
69
70 const afterClose = committedOpen.length;
71 await renderMenu(false);
72 expect(committedOpen).toHaveLength(afterClose);
73 expect(committedOpen.at(-1)).toBe(false);
74
75 await act(() => setOpen(true));
76 expect(committedOpen.at(-1)).toBe(true);
77 await act(() => setOpen(false));
78 expect(committedOpen.at(-1)).toBe(false);
79 expect(closePopup).toHaveBeenCalledTimes(2);
80 });
81
82 it("does not commit an open popup requested while its trigger is hidden", async () => {
83 await renderMenu(true);
84 await act(() => setOpen(true));
85 await renderMenu(false);
86
87 expect(committedOpen.every((open) => !open)).toBe(true);
88 expect(closePopup).not.toHaveBeenCalled();
89 });
90});