返回 DeepSeek-Reasonix
workspace-resize-interaction.test.tsx
根目录 / desktop / frontend / src / __tests__ / workspace-resize-interaction.test.tsx
1 // Run: tsx src/__tests__/workspace-resize-interaction.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 { WorkspacePanel } from "../components/WorkspacePanel";
8 import type { AppBindings } from "../lib/bridge";
9 import { LocaleProvider } from "../lib/i18n";
10 import { resetWorkspaceTreeMemoryForTests } from "../lib/workspaceTreeMemory";
11 import { installDesktopHostStub } from "./desktopHostStub";
12
13 let passed = 0;
14 let failed = 0;
15
16 function eq<T>(actual: T, expected: T, label: string) {
17 if (Object.is(actual, expected)) {
18 process.stdout.write(` PASS ${label}\n`);
19 passed += 1;
20 } else {
21 process.stdout.write(` FAIL ${label}: expected ${String(expected)}, got ${String(actual)}\n`);
22 failed += 1;
23 }
24 }
25
26 function flushTimers(): Promise<void> {
27 return new Promise((resolve) => setTimeout(resolve, 0));
28 }
29
30 async function waitFor(label: string, predicate: () => boolean) {
31 for (let attempt = 0; attempt < 30; attempt += 1) {
32 await act(async () => {
33 await flushTimers();
34 });
35 if (predicate()) return;
36 }
37 throw new Error(`timed out waiting for ${label}`);
38 }
39
40 class TestResizeObserver {
41 observe() {}
42 unobserve() {}
43 disconnect() {}
44 }
45
46 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
47 pretendToBeVisual: true,
48 url: "http://localhost/",
49 });
50 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
51 globalThis.window = dom.window as unknown as Window & typeof globalThis;
52 globalThis.document = dom.window.document;
53 Object.defineProperty(dom.window.navigator, "language", { configurable: true, value: "en-US" });
54 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
55 globalThis.Node = dom.window.Node;
56 globalThis.Element = dom.window.Element;
57 globalThis.HTMLElement = dom.window.HTMLElement;
58 globalThis.Event = dom.window.Event;
59 globalThis.CustomEvent = dom.window.CustomEvent;
60 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
61 globalThis.MouseEvent = dom.window.MouseEvent;
62 globalThis.PointerEvent = dom.window.MouseEvent as unknown as typeof PointerEvent;
63 globalThis.MutationObserver = dom.window.MutationObserver;
64 globalThis.ResizeObserver = TestResizeObserver;
65 dom.window.ResizeObserver = TestResizeObserver;
66 globalThis.localStorage = dom.window.localStorage;
67 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
68 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
69 Object.defineProperty(dom.window.HTMLElement.prototype, "scrollIntoView", { configurable: true, value: () => {} });
70 Object.defineProperty(dom.window.HTMLElement.prototype, "offsetWidth", { configurable: true, get: () => 800 });
71 Object.defineProperty(dom.window.HTMLElement.prototype, "offsetHeight", {
72 configurable: true,
73 get: function offsetHeight(this: HTMLElement) {
74 return this.classList.contains("workspace-tree") ? 300 : this.dataset.index ? 24 : 0;
75 },
76 });
77 Object.defineProperty(dom.window.HTMLElement.prototype, "getBoundingClientRect", {
78 configurable: true,
79 value: function getBoundingClientRect(this: HTMLElement) {
80 const width = 800;
81 const height = this.classList.contains("workspace-tree") ? 300 : this.dataset.index ? 24 : 0;
82 return { x: 100, y: 0, top: 0, left: 100, right: 900, bottom: height, width, height, toJSON: () => ({}) } as DOMRect;
83 },
84 });
85
86 console.log("\nworkspace right-side tree resize interaction");
87
88 resetWorkspaceTreeMemoryForTests();
89 installDesktopHostStub(({
90 main: {
91 App: {
92 ListDirForTab: async (_tabId, dir) => dir === "" ? [{ name: "app.ts", isDir: false }] : [],
93 SearchFileRefsForTab: async () => [],
94 WorkspaceGitHistory: async () => [],
95 WorkspaceChanges: async () => ({ files: [], gitAvailable: true }),
96 WorkspaceChangeDetail: async () => ({}),
97 ResolveWorkspacePathForTab: async (_tabId, path) => path.startsWith("/") ? path : `/repo/${path}`,
98 ReadFileForTab: async (_tabId, path) => ({ path, body: "const value = 1;", size: 16, truncated: false, binary: false }),
99 } as Partial<AppBindings> as AppBindings,
100 },
101 }).main.App);
102
103 const rootElement = document.getElementById("root");
104 if (!rootElement) throw new Error("missing root");
105 const root = createRoot(rootElement);
106 await act(async () => {
107 root.render(
108 <LocaleProvider>
109 <WorkspacePanel
110 open
111 tabId="resize-tab"
112 cwd="/repo"
113 maximized={false}
114 panelWidth={800}
115 initialViewMode="files"
116 onClose={() => {}}
117 onToggleMaximized={() => {}}
118 />
119 </LocaleProvider>,
120 );
121 await flushTimers();
122 });
123
124 await waitFor("workspace file", () => document.querySelector('[data-workspace-path="app.ts"]') !== null);
125 await act(async () => {
126 document.querySelector<HTMLButtonElement>('[data-workspace-path="app.ts"]')?.click();
127 await flushTimers();
128 });
129 await waitFor("tree separator", () => document.querySelector(".workspace-tree-resizer") !== null);
130
131 const separator = document.querySelector<HTMLButtonElement>(".workspace-tree-resizer");
132 if (!separator) throw new Error("missing tree separator");
133 const initialWidth = Number(separator.getAttribute("aria-valuenow"));
134
135 await act(async () => {
136 separator.dispatchEvent(new window.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowRight" }));
137 await flushTimers();
138 });
139 eq(Number(separator.getAttribute("aria-valuenow")), initialWidth - 16, "ArrowRight moves the separator right and narrows the right-side tree");
140
141 await act(async () => {
142 separator.dispatchEvent(new window.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "ArrowLeft" }));
143 await flushTimers();
144 });
145 eq(Number(separator.getAttribute("aria-valuenow")), initialWidth, "ArrowLeft moves the separator left and widens the right-side tree");
146
147 await act(async () => {
148 separator.dispatchEvent(new window.MouseEvent("pointerdown", { bubbles: true, cancelable: true, clientX: 600 }));
149 window.dispatchEvent(new window.MouseEvent("pointermove", { bubbles: true, clientX: 650 }));
150 window.dispatchEvent(new window.MouseEvent("pointerup", { bubbles: true, clientX: 650 }));
151 await flushTimers();
152 });
153 eq(Number(separator.getAttribute("aria-valuenow")), 250, "pointer position measures tree width from the panel's right edge");
154
155 await act(async () => {
156 separator.dispatchEvent(new window.KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "End" }));
157 await flushTimers();
158 });
159 eq(Number(separator.getAttribute("aria-valuenow")), 660, "End widens the tree to keep only the preview minimum");
160
161 await act(async () => {
162 root.unmount();
163 });
164 dom.window.close();
165
166 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
167 if (failed > 0) process.exit(1);
168
168 lines Plain Text