返回 DeepSeek-Reasonix
anchored-overlay-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / anchored-overlay-lifecycle.test.tsx
1 // Run: tsx src/__tests__/anchored-overlay-lifecycle.test.tsx
2
3 import assert from "node:assert/strict";
4 import { JSDOM } from "jsdom";
5 import React, { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import { ComposerChoice } from "../components/ComposerChoice";
8 import { Tooltip } from "../components/Tooltip";
9
10 const dom = new JSDOM('<div id="root"></div>', { pretendToBeVisual: true, url: "http://localhost/" });
11 Object.assign(globalThis, {
12 window: dom.window, document: dom.window.document, HTMLElement: dom.window.HTMLElement, Node: dom.window.Node,
13 MouseEvent: dom.window.MouseEvent, KeyboardEvent: dom.window.KeyboardEvent,
14 IS_REACT_ACT_ENVIRONMENT: true,
15 });
16 window.matchMedia = (() => ({ matches: true, addEventListener() {}, removeEventListener() {} })) as typeof window.matchMedia;
17 let nextFrameId = 1;
18 const pendingFrames = new Map<number, FrameRequestCallback>();
19 const requestFrame = ((callback: FrameRequestCallback) => {
20 const id = nextFrameId++;
21 pendingFrames.set(id, callback);
22 return id;
23 }) as typeof window.requestAnimationFrame;
24 const cancelFrame = ((id: number) => pendingFrames.delete(id)) as typeof window.cancelAnimationFrame;
25 window.requestAnimationFrame = requestFrame;
26 window.cancelAnimationFrame = cancelFrame;
27 Object.assign(globalThis, { requestAnimationFrame: requestFrame, cancelAnimationFrame: cancelFrame });
28 const originalRect = dom.window.HTMLElement.prototype.getBoundingClientRect;
29 let tooltipHasLayout = true;
30 dom.window.HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
31 if (this.matches("button, .tooltip-trigger")) return new dom.window.DOMRect(300, 500, 160, 32);
32 if (this.matches("[data-anchored-popover]")) return new dom.window.DOMRect(0, 0, 240, 100);
33 if (this.matches("[role=tooltip]")) return tooltipHasLayout
34 ? new dom.window.DOMRect(0, 0, 80, 28)
35 : new dom.window.DOMRect(0, 0, 0, 0);
36 return originalRect.call(this);
37 };
38 async function settle() {
39 await act(async () => {
40 await new Promise((resolve) => setTimeout(resolve, 1));
41 });
42 await act(async () => {
43 const callbacks = [...pendingFrames.values()];
44 pendingFrames.clear();
45 callbacks.forEach((callback) => callback(performance.now()));
46 });
47 }
48
49 const root = createRoot(document.getElementById("root")!);
50 async function renderChoice(disabled: boolean, dismissSignal = 0) {
51 await act(async () => root.render(<ComposerChoice label="Effort" value="a" disabled={disabled}
52 dismissSignal={dismissSignal}
53 options={[{ value: "a", label: "A" }, { value: "b", label: "B" }]} onPick={() => {}} />));
54 }
55 await renderChoice(false);
56 await act(async () => document.querySelector<HTMLButtonElement>(".composer-choice")!.click());
57 assert.ok(document.querySelector("[data-anchored-popover]"), "choice opens its menu");
58 await renderChoice(true);
59 await settle();
60 assert.equal(document.querySelector("[data-anchored-popover]"), null, "disabled closes the menu");
61 await renderChoice(false);
62 assert.equal(document.querySelector("[data-anchored-popover]"), null, "disabled then enabled does not reopen the menu");
63 assert.equal(document.querySelector(".composer-choice")?.getAttribute("aria-expanded"), "false");
64
65 await act(async () => document.querySelector<HTMLButtonElement>(".composer-choice")!.click());
66 assert.ok(document.querySelector("[data-anchored-popover]"), "choice reopens after becoming enabled");
67 await renderChoice(false, 1);
68 await settle();
69 assert.equal(document.querySelector("[data-anchored-popover]"), null, "dismiss signal closes the choice menu");
70 await renderChoice(false, 2);
71 assert.equal(document.querySelector("[data-anchored-popover]"), null, "a later dismiss signal does not reopen the menu");
72
73 await act(async () => root.render(<div id="tooltip-host"><Tooltip label="Help" delay={0}><button>Info</button></Tooltip></div>));
74 const trigger = document.querySelector<HTMLElement>(".tooltip-trigger")!;
75 await act(async () => trigger.dispatchEvent(new dom.window.MouseEvent("mouseover", { bubbles: true })));
76 await settle();
77 assert.ok(document.querySelector("[role=tooltip]"), "tooltip opens from a valid anchor");
78 await act(async () => { (document.getElementById("tooltip-host") as HTMLElement).hidden = true; });
79 await settle();
80 assert.equal(document.querySelector("[role=tooltip]"), null, "tooltip closes when its anchor becomes hidden");
81
82 await act(async () => {
83 (document.getElementById("tooltip-host") as HTMLElement).hidden = false;
84 trigger.dispatchEvent(new dom.window.MouseEvent("mouseover", { bubbles: true }));
85 (document.getElementById("tooltip-host") as HTMLElement).hidden = true;
86 });
87 await settle();
88 assert.equal(document.querySelector("[role=tooltip]"), null, "hidden anchor cancels delayed tooltip display");
89
90 await act(async () => { (document.getElementById("tooltip-host") as HTMLElement).hidden = false; });
91 tooltipHasLayout = false;
92 await act(async () => trigger.dispatchEvent(new dom.window.MouseEvent("mouseover", { bubbles: true })));
93 await settle();
94 assert.equal((document.querySelector<HTMLElement>("[role=tooltip]")?.style.visibility), "hidden", "unmeasured tooltip stays hidden");
95 tooltipHasLayout = true;
96 await settle();
97 assert.equal((document.querySelector<HTMLElement>("[role=tooltip]")?.style.visibility), "visible", "tooltip becomes visible after delayed layout");
98
99 await act(async () => root.unmount());
100 dom.window.close();
101 console.log("PASS anchored overlay owner state and tooltip lifecycle");
102
102 lines Plain Text