1import { cloneElement, type ReactElement, type ReactNode } from "react";
2import { act, create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, expect, it, vi } from "vite-plus/test";
4
5import { ComposerBannerStack } from "./ComposerBannerStack";
6
7vi.mock("../ui/popover", () => ({
8 Popover: "popover",
9 PopoverTrigger: ({ render, children }: { render: ReactElement; children: ReactNode }) =>
10 cloneElement(render, {}, children),
11 PopoverPopup: "popup",
12}));
13vi.mock("../ui/button", () => ({ Button: "button" }));
14vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" }));
15
16let renderer: ReactTestRenderer;
17afterEach(async () => {
18 if (renderer) await act(() => renderer.unmount());
19 vi.unstubAllGlobals();
20});
21
22it("only offers notice details when the description cannot fit", async () => {
23 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
24 let resize = () => {};
25 let mutate = () => {};
26 vi.stubGlobal(
27 "MutationObserver",
28 class {
29 constructor(callback: () => void) {
30 mutate = callback;
31 }
32 observe() {}
33 disconnect() {}
34 },
35 );
36 vi.stubGlobal(
37 "ResizeObserver",
38 class {
39 constructor(private readonly callback: (entries: { target: unknown }[]) => void) {}
40 observe(target: unknown) {
41 resize = () => this.callback([{ target }]);
42 }
43 disconnect() {}
44 },
45 );
46 let position = "static";
47 vi.stubGlobal("getComputedStyle", () => ({ position }));
48 let availableWidth = 200;
49 const nested = { clientWidth: 100, scrollWidth: 80 };
50 const text = {
51 querySelectorAll: () => [nested],
52 get clientWidth() {
53 return (
54 availableWidth -
55 (renderer?.root.findAllByProps({ "aria-label": "Show notice details" }).length ? 28 : 0)
56 );
57 },
58 scrollWidth: 80,
59 };
60 await act(() => {
61 renderer = create(
62 <ComposerBannerStack
63 items={[
64 {
65 id: "usage",
66 variant: "info",
67 icon: null,
68 title: "Usage limits",
69 description: "OpenCode",
70 },
71 ]}
72 />,
73 {
74 createNodeMock: (element) =>
75 element.type === "span" ? text : element.type === "button" ? { offsetWidth: 24 } : null,
76 },
77 );
78 });
79 const details = () => renderer.root.findAllByProps({ "aria-label": "Show notice details" });
80 expect(details()).toHaveLength(0);
81 text.scrollWidth = 300;
82 await act(() => resize());
83 expect(details()).toHaveLength(1);
84 // It fits without the icon: the icon must not keep its own overflow alive.
85 availableWidth = 308;
86 await act(() => resize());
87 expect(details()).toHaveLength(0);
88 text.scrollWidth = 80;
89 await act(() => resize());
90 expect(details()).toHaveLength(0);
91 nested.scrollWidth = 500;
92 await act(() => mutate());
93 expect(details()).toHaveLength(1);
94 nested.scrollWidth = 80;
95 await act(() => mutate());
96 expect(details()).toHaveLength(0);
97 position = "absolute";
98 await act(() => resize());
99 expect(details()).toHaveLength(1);
100 position = "static";
101 await act(() => resize());
102 expect(details()).toHaveLength(0);
103});