返回 DeepSeek-Reasonix
virtual-menu-identity.test.tsx
根目录 / desktop / frontend / src / __tests__ / virtual-menu-identity.test.tsx
1 // Run: tsx src/__tests__/virtual-menu-identity.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React from "react";
5 import { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import { SlashMenu } from "../components/SlashMenu";
8 import { LocaleProvider } from "../lib/i18n";
9 import type { CommandInfo } from "../lib/types";
10
11 let passed = 0;
12 let failed = 0;
13
14 function ok(value: boolean, label: string) {
15 if (value) {
16 process.stdout.write(` PASS ${label}\n`);
17 passed += 1;
18 } else {
19 process.stdout.write(` FAIL ${label}\n`);
20 failed += 1;
21 }
22 }
23
24 function rect(height: number): DOMRect {
25 return {
26 left: 0,
27 top: 0,
28 right: 600,
29 bottom: height,
30 width: 600,
31 height,
32 x: 0,
33 y: 0,
34 toJSON: () => ({}),
35 } as DOMRect;
36 }
37
38 class TestResizeObserver {
39 observe() {}
40 unobserve() {}
41 disconnect() {}
42 }
43
44 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
45 pretendToBeVisual: true,
46 url: "http://localhost/",
47 });
48
49 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
50 globalThis.window = dom.window as unknown as Window & typeof globalThis;
51 globalThis.document = dom.window.document;
52 globalThis.Element = dom.window.Element;
53 globalThis.HTMLElement = dom.window.HTMLElement;
54 globalThis.Node = dom.window.Node;
55 globalThis.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver;
56 dom.window.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver;
57 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
58 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
59
60 Object.defineProperty(dom.window.HTMLElement.prototype, "getBoundingClientRect", {
61 configurable: true,
62 value(this: HTMLElement) {
63 if (this.classList.contains("slashmenu")) return rect(360);
64 if (this.classList.contains("slashmenu__row")) {
65 return rect(this.firstElementChild?.classList.contains("slashmenu__group") ? 26 : 34);
66 }
67 return rect(0);
68 },
69 });
70 Object.defineProperty(dom.window.HTMLElement.prototype, "offsetWidth", {
71 configurable: true,
72 get() {
73 return 600;
74 },
75 });
76 Object.defineProperty(dom.window.HTMLElement.prototype, "offsetHeight", {
77 configurable: true,
78 get(this: HTMLElement) {
79 if (this.classList.contains("slashmenu")) return 360;
80 if (this.classList.contains("slashmenu__row")) {
81 return this.firstElementChild?.classList.contains("slashmenu__group") ? 26 : 34;
82 }
83 return 0;
84 },
85 });
86
87 const initialItems: CommandInfo[] = [
88 { name: "documentation", description: "Documentation", kind: "skill" },
89 { name: "docx", description: "Word documents", kind: "skill" },
90 { name: "lark-doc", description: "Lark documents", kind: "skill" },
91 { name: "grill-with-docs", description: "Review with docs", kind: "skill" },
92 { name: "tencent-docs", description: "Tencent documents", kind: "skill" },
93 { name: "docs", description: "Search bundled documentation", kind: "builtin", group: "integrations" },
94 ];
95
96 const filteredItems = [initialItems[0]!, initialItems[4]!, initialItems[5]!];
97
98 function Menu({ items }: { items: CommandInfo[] }) {
99 return (
100 <LocaleProvider>
101 <SlashMenu items={items} activeIndex={-1} onPick={() => {}} onHover={() => {}} />
102 </LocaleProvider>
103 );
104 }
105
106 function rowPositions(): Array<{ label: string; position: number; transform: string }> {
107 return Array.from(document.querySelectorAll<HTMLElement>(".slashmenu__row")).map((row) => {
108 const match = row.style.transform.match(/translate3d\(0(?:px)?, ([\d.-]+)px, 0(?:px)?\)/);
109 const name = row.querySelector(".slashmenu__name")?.textContent;
110 return {
111 label: name ?? "group",
112 position: match ? Number(match[1]) : Number.NaN,
113 transform: row.style.transform,
114 };
115 });
116 }
117
118 console.log("\nvirtual menu item identity");
119
120 const rootElement = document.getElementById("root");
121 if (!rootElement) throw new Error("missing root");
122 const root = createRoot(rootElement);
123
124 await act(async () => {
125 root.render(<Menu items={initialItems} />);
126 await new Promise((resolve) => setTimeout(resolve, 0));
127 });
128 const initialDocsRow = Array.from(document.querySelectorAll<HTMLElement>(".slashmenu__row"))
129 .find((row) => row.querySelector(".slashmenu__name")?.textContent === "/docs");
130 await act(async () => {
131 root.render(<Menu items={filteredItems} />);
132 await new Promise((resolve) => setTimeout(resolve, 0));
133 });
134
135 const positions = rowPositions();
136 ok(
137 JSON.stringify(positions) === JSON.stringify([
138 { label: "group", position: 0, transform: "translate3d(0, 0px, 0)" },
139 { label: "/documentation", position: 26, transform: "translate3d(0, 26px, 0)" },
140 { label: "/tencent-docs", position: 60, transform: "translate3d(0, 60px, 0)" },
141 { label: "group", position: 94, transform: "translate3d(0, 94px, 0)" },
142 { label: "/docs", position: 120, transform: "translate3d(0, 120px, 0)" },
143 ]),
144 `filtered rows keep contiguous semantic positions: got ${JSON.stringify(positions)}`,
145 );
146 const filteredDocsRow = Array.from(document.querySelectorAll<HTMLElement>(".slashmenu__row"))
147 .find((row) => row.querySelector(".slashmenu__name")?.textContent === "/docs");
148 ok(initialDocsRow === filteredDocsRow, "/docs keeps the same semantic DOM row while its filtered index changes");
149
150 await act(async () => root.unmount());
151 dom.window.close();
152
153 console.log(`\n${passed} passed, ${failed} failed`);
154 if (failed > 0) process.exit(1);
155
155 lines Plain Text