返回 DeepSeek-Reasonix
workspace-changes-errors.test.tsx
根目录 / desktop / frontend / src / __tests__ / workspace-changes-errors.test.tsx
1 // Run: tsx src/__tests__/workspace-changes-errors.test.tsx
2
3 import { act } from "react";
4 import { workspaceFileIcon } from "../components/WorkspaceFileIcon";
5 import type { DirEntry, FilePreview, WorkspaceChangeDetailView, WorkspaceChangesView } from "../lib/types";
6 import { flushPromises, renderFilesWorkspace, renderWorkspace, waitFor } from "./workspace-panel-test-harness";
7 import { performResourceAction } from "../lib/fileNavigationCommands";
8
9 let passed = 0;
10 let failed = 0;
11
12 function ok(value: boolean, label: string) {
13 if (value) {
14 process.stdout.write(` PASS ${label}\n`);
15 passed += 1;
16 } else {
17 process.stdout.write(` FAIL ${label}\n`);
18 failed += 1;
19 }
20 }
21
22 console.log("\nworkspace changes git errors");
23
24 {
25 const { dom, root, rerender } = await renderFilesWorkspace({}, { open: false });
26 let threw = false;
27 try {
28 await rerender({ open: true });
29 } catch (error) {
30 threw = true;
31 process.stdout.write(` ERROR ${String(error)}\n`);
32 }
33 ok(!threw, "workspace panel can open after a closed render without changing hook order");
34 ok(document.querySelector(".workspace-panel") !== null, "workspace panel renders after the closed-to-open transition");
35 await act(async () => {
36 root.unmount();
37 });
38 dom.window.close();
39 }
40
41 {
42 const { dom, root } = await renderWorkspace({ files: [], gitAvailable: false });
43 await waitFor("git unavailable warning", () => document.body.textContent?.includes("Git status is unavailable for this workspace.") === true);
44 ok(document.body.textContent?.includes("Git status is unavailable for this workspace.") === true, "gitAvailable=false renders a warning");
45 ok(document.body.textContent?.includes("No changed files") === false, "gitAvailable=false is not shown as a clean workspace");
46 await act(async () => {
47 root.unmount();
48 });
49 dom.window.close();
50 }
51
52 {
53 const { dom, root } = await renderWorkspace(
54 { files: [], gitAvailable: true },
55 {
56 completionSummary: {
57 preset: "balanced",
58 verdict: "partial",
59 mutations: 3,
60 checks_passed: 12,
61 checks_failed: 1,
62 checks_suppressed: 2,
63 review: "passed",
64 gap_kinds: ["stale_check", "future_internal_value"],
65 constraint_degraded: true,
66 },
67 },
68 );
69 await waitFor("workspace changes without a turn request", () => document.body.textContent?.includes("No changed files") === true);
70 ok(document.querySelector(".workspace-completion-summary") === null, "whole workspace does not imply that the latest turn verifies all files");
71 ok(document.querySelector(".workspace-turn-result") === null, "turn inspection requires its explicit view request");
72 const text = document.body.textContent ?? "";
73 ok(!text.includes("balanced") && !text.includes("stale_check") && !text.includes("future_internal_value"), "whole workspace exposes no completion enum values");
74 await act(async () => {
75 root.unmount();
76 });
77 dom.window.close();
78 }
79
80 {
81 const { dom, root } = await renderWorkspace({
82 files: [],
83 gitAvailable: true,
84 gitErr: "git status timed out",
85 });
86 await waitFor("git error warning without files", () => document.body.textContent?.includes("Git status is unavailable for this workspace.") === true);
87 ok(document.body.textContent?.includes("Git status is unavailable for this workspace.") === true, "gitErr without files renders a warning");
88 ok(document.body.textContent?.includes("No changed files") === false, "empty files plus gitErr is not shown as a clean workspace");
89 await act(async () => {
90 root.unmount();
91 });
92 dom.window.close();
93 }
94
95 {
96 const { dom, root } = await renderWorkspace({
97 files: [
98 {
99 path: "src/app.ts",
100 sources: ["session"],
101 gitStatus: "modified",
102 latestPrompt: "edit app",
103 },
104 ],
105 gitAvailable: true,
106 gitErr: "git status timed out",
107 });
108 await waitFor("git error warning with files", () => document.body.textContent?.includes("app.ts") === true);
109 ok(document.body.textContent?.includes("Git status is unavailable for this workspace.") === true, "gitErr renders a warning");
110 ok(document.body.textContent?.includes("app.ts") === true, "files still render when gitErr is present");
111 await act(async () => {
112 root.unmount();
113 });
114 dom.window.close();
115 }
116
117 {
118 const { dom, root } = await renderWorkspace(
119 {
120 files: [
121 { path: "src/session.ts", sources: ["session"], gitStatus: "M", latestPrompt: "edit session file" },
122 { path: "README.md", sources: ["git"], gitStatus: "M" },
123 ],
124 gitAvailable: true,
125 },
126 {
127 history: [{ hash: "1234567890", author: "Agent", date: "2026-07-10T12:00:00Z", message: "older commit" }],
128 },
129 );
130 await waitFor("creation changes sections", () => document.body.textContent?.includes("Session changes") === true);
131 ok(document.body.textContent?.includes("Session changes") === true, "Creation changes prioritizes session files");
132 ok(document.body.textContent?.includes("Uncommitted workspace changes") === true, "Creation changes keeps git-only files separate");
133 ok(document.body.textContent?.includes("Commit history") === true, "Creation changes exposes commit history as a secondary section");
134 ok(document.body.textContent?.includes("older commit") === false, "Creation commit history starts collapsed");
135 const historyToggle = document.querySelector<HTMLButtonElement>(".workspace-commit-history__toggle");
136 await act(async () => {
137 historyToggle?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
138 await flushPromises();
139 });
140 await waitFor("expanded creation commit history", () => document.body.textContent?.includes("older commit") === true);
141 ok(document.body.textContent?.includes("older commit") === true, "Creation commit history expands on demand");
142 await act(async () => {
143 root.unmount();
144 });
145 dom.window.close();
146 }
147
148 {
149 const { dom, root } = await renderWorkspace(
150 {
151 files: [{ path: "src/current.ts", sources: ["git"], gitStatus: "M" }],
152 gitAvailable: true,
153 },
154 {
155 history: [{ hash: "abcdef123456", author: "Agent", date: "2026-07-20T12:00:00Z", message: "historical commit" }],
156 detail: {
157 source: "git",
158 added: 2,
159 removed: 1,
160 diff: "diff --git a/src/current.ts b/src/current.ts\n--- a/src/current.ts\n+++ b/src/current.ts\n@@ -10,2 +10,3 @@\n-old value\n+new value\n context\n+another value",
161 },
162 },
163 );
164 await waitFor("git-only working change", () => document.body.textContent?.includes("current.ts") === true);
165 ok(document.body.textContent?.includes("No changed files") === false, "git-only working changes are not reported as a clean workspace");
166 const changeButton = document.querySelector<HTMLButtonElement>(".workspace-change");
167 await act(async () => {
168 changeButton?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
169 await flushPromises();
170 });
171 await waitFor("current semantic diff", () => document.body.textContent?.includes("new value") === true);
172 ok(document.body.textContent?.includes("Current changes") === true, "selected working file shows the current patch before history");
173 ok(document.body.textContent?.includes("+2") === true && document.body.textContent?.includes("-1") === true, "current patch shows added and removed line totals");
174 ok(document.body.textContent?.includes("historical commit") === false, "file commit history starts collapsed");
175 const historyToggle = document.querySelector<HTMLButtonElement>(".workspace-commit-history__toggle");
176 await act(async () => {
177 historyToggle?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
178 await flushPromises();
179 });
180 await waitFor("file commit history", () => document.body.textContent?.includes("historical commit") === true);
181 ok(document.body.textContent?.includes("historical commit") === true, "file commit history remains available on demand");
182 await act(async () => {
183 root.unmount();
184 });
185 dom.window.close();
186 }
187
188 {
189 const { dom, root } = await renderWorkspace(
190 {
191 files: [{ path: "generated/large.txt", sources: ["git"], gitStatus: "M" }],
192 gitAvailable: true,
193 },
194 { detail: { source: "git", truncated: true } },
195 );
196 await waitFor("large working change", () => document.body.textContent?.includes("large.txt") === true);
197 const changeButton = document.querySelector<HTMLButtonElement>(".workspace-change");
198 await act(async () => {
199 changeButton?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
200 await flushPromises();
201 });
202 await waitFor("bounded change detail", () => document.body.textContent?.includes("too large to display") === true);
203 ok(document.body.textContent?.includes("too large to display") === true, "oversized workspace diffs render a bounded-state message");
204 ok(document.body.textContent?.includes("no text diff") === false, "oversized workspace diffs are not reported as empty");
205 await act(async () => {
206 root.unmount();
207 });
208 dom.window.close();
209 }
210
211 {
212 const { dom, root } = await renderFilesWorkspace({
213 ListDirForTab: async (_tabId, dir) => {
214 if (dir === "") {
215 return [
216 { name: "src", isDir: true },
217 { name: "tail-a.ts", isDir: false },
218 { name: "tail-b.ts", isDir: false },
219 ];
220 }
221 if (dir === "src/") {
222 return [
223 { name: "child-a.ts", isDir: false },
224 { name: "child-b.ts", isDir: false },
225 ];
226 }
227 return [];
228 },
229 });
230
231 const positionedRows = () =>
232 Array.from(document.querySelectorAll<HTMLElement>(".workspace-tree__sizer > div")).map((wrapper) => ({
233 path: wrapper.querySelector<HTMLElement>("[data-workspace-path]")?.dataset.workspacePath ?? "",
234 transform: wrapper.style.transform,
235 }));
236 const positionsAreUnique = (paths: string[]) => {
237 const rows = positionedRows().filter((row) => paths.includes(row.path));
238 return rows.length === paths.length && new Set(rows.map((row) => row.transform)).size === paths.length;
239 };
240
241 const collapsedPaths = ["src/", "tail-a.ts", "tail-b.ts"];
242 await waitFor("initial positioned workspace rows", () => positionsAreUnique(collapsedPaths));
243
244 const toggleSrc = () => document.querySelector<HTMLButtonElement>('[data-workspace-path="src/"]');
245 await act(async () => {
246 toggleSrc()?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
247 await flushPromises();
248 });
249 const expandedPaths = ["src/", "src/child-a.ts", "src/child-b.ts", "tail-a.ts", "tail-b.ts"];
250 await waitFor("expanded workspace rows", () => document.body.textContent?.includes("child-b.ts") === true);
251 ok(positionsAreUnique(expandedPaths), "expanded workspace rows keep unique virtual positions");
252
253 await act(async () => {
254 toggleSrc()?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
255 await flushPromises();
256 });
257 await waitFor("collapsed workspace rows", () => document.body.textContent?.includes("child-a.ts") === false);
258 ok(positionsAreUnique(collapsedPaths), "collapsed workspace rows keep unique virtual positions");
259
260 await act(async () => {
261 toggleSrc()?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
262 await flushPromises();
263 });
264 await waitFor("re-expanded workspace rows", () => document.body.textContent?.includes("child-b.ts") === true);
265 ok(positionsAreUnique(expandedPaths), "re-expanded workspace rows keep unique virtual positions");
266
267 await act(async () => {
268 root.unmount();
269 });
270 dom.window.close();
271 }
272
273 {
274 const calls: string[] = [];
275 const listDirForTab = async (tabId: string, dir: string): Promise<DirEntry[]> => {
276 calls.push(`${tabId}:${dir}`);
277 return [];
278 };
279 const { dom, root, rerender } = await renderFilesWorkspace(
280 { ListDirForTab: listDirForTab },
281 { fileListRequest: { id: 1, paths: ["src/app.ts"] } },
282 );
283
284 await waitFor("initial referenced file dirs", () => calls.filter((call) => call === "tab-a:src/").length === 1);
285 await rerender({ fileListRequest: { id: 2, paths: ["src/app.ts"] } });
286 await waitFor("referenced file dirs revalidated", () => calls.filter((call) => call === "tab-a:src/").length === 2);
287
288 ok(calls.filter((call) => call === "tab-a:src/").length === 2, "workspace file tree revalidates cached directories for repeated file-list requests");
289
290 await act(async () => {
291 root.unmount();
292 });
293 dom.window.close();
294 }
295
296 {
297 const pending: Array<{ tabId: string; resolve: (entries: DirEntry[]) => void }> = [];
298 const listDirForTab = (tabId: string, dir: string): Promise<DirEntry[]> => {
299 if (dir !== "") return Promise.resolve([]);
300 return new Promise((resolve) => pending.push({ tabId, resolve }));
301 };
302 const { dom, root, rerender } = await renderFilesWorkspace(
303 { ListDirForTab: listDirForTab },
304 { tabId: "parent-tab", cwd: "/repo" },
305 );
306
307 await waitFor("parent workspace request", () => pending.some((request) => request.tabId === "parent-tab"));
308 await rerender({ tabId: "child-tab", cwd: "/repo/child" });
309 await waitFor("child workspace request", () => pending.some((request) => request.tabId === "child-tab"));
310
311 await act(async () => {
312 pending.filter((request) => request.tabId === "child-tab").forEach((request) => request.resolve([
313 { name: "child-a.txt", isDir: false },
314 { name: "child-b.txt", isDir: false },
315 ]));
316 await flushPromises();
317 });
318 await waitFor("child workspace entries", () => (document.querySelector(".workspace-tree__sizer") as HTMLElement | null)?.style.height === "48px");
319
320 await act(async () => {
321 pending.filter((request) => request.tabId === "parent-tab").forEach((request) => request.resolve([{ name: "parent-only.txt", isDir: false }]));
322 await flushPromises();
323 });
324
325 ok((document.querySelector(".workspace-tree__sizer") as HTMLElement | null)?.style.height === "48px", "late parent workspace response cannot overwrite the two-row child tree");
326
327 await act(async () => {
328 root.unmount();
329 });
330 dom.window.close();
331 }
332
333 {
334 const pending: Array<(entries: DirEntry[]) => void> = [];
335 const listDirForTab = (_tabId: string, dir: string): Promise<DirEntry[]> => {
336 if (dir !== "") return Promise.resolve([]);
337 return new Promise((resolve) => pending.push(resolve));
338 };
339 const { dom, root, rerender } = await renderFilesWorkspace(
340 { ListDirForTab: listDirForTab },
341 { tabId: "shared-tab", cwd: "/repo", workspaceScopeKey: "session-a" },
342 );
343
344 await waitFor("initial session A workspace request", () => pending.length === 1);
345 await rerender({ workspaceScopeKey: "session-b" });
346 await waitFor("session B workspace request", () => pending.length === 2);
347 await rerender({ workspaceScopeKey: "session-a" });
348 await waitFor("revisited session A workspace request", () => pending.length === 3);
349
350 await act(async () => {
351 pending[2]([
352 { name: "current-a.txt", isDir: false },
353 { name: "current-b.txt", isDir: false },
354 ]);
355 await flushPromises();
356 });
357 await waitFor("revisited session A entries", () => (document.querySelector(".workspace-tree__sizer") as HTMLElement | null)?.style.height === "48px");
358
359 await act(async () => {
360 pending[0]([{ name: "stale-initial-a.txt", isDir: false }]);
361 pending[1]([{ name: "stale-b.txt", isDir: false }]);
362 await flushPromises();
363 });
364
365 ok(
366 (document.querySelector(".workspace-tree__sizer") as HTMLElement | null)?.style.height === "48px",
367 "same-tab A→B→A session switches reject stale workspace responses",
368 );
369
370 await act(async () => {
371 root.unmount();
372 });
373 dom.window.close();
374 }
375
376 {
377 const pending: Array<(changes: WorkspaceChangesView) => void> = [];
378 const workspaceChanges = (): Promise<WorkspaceChangesView> => new Promise((resolve) => pending.push(resolve));
379 const { dom, root, rerender } = await renderFilesWorkspace(
380 { WorkspaceChanges: workspaceChanges },
381 {
382 tabId: "shared-tab",
383 cwd: "/repo",
384 workspaceScopeKey: "session-a",
385 initialViewMode: "changed",
386 },
387 );
388
389 await waitFor("initial session changes request", () => pending.length === 1);
390 await rerender({ workspaceScopeKey: "session-b" });
391 await waitFor("next session changes request", () => pending.length === 2);
392
393 await act(async () => {
394 pending[1]({
395 files: [{ path: "session-b.ts", sources: ["session"] }],
396 gitAvailable: true,
397 });
398 await flushPromises();
399 });
400 await waitFor("session B changes", () => document.body.textContent?.includes("session-b.ts") === true);
401
402 await act(async () => {
403 pending[0]({
404 files: [{ path: "stale-session-a.ts", sources: ["session"] }],
405 gitAvailable: true,
406 });
407 await flushPromises();
408 });
409
410 ok(document.body.textContent?.includes("session-b.ts") === true, "current same-tab session changes stay visible");
411 ok(document.body.textContent?.includes("stale-session-a.ts") === false, "late same-tab session changes cannot overwrite the current session");
412
413 await act(async () => {
414 root.unmount();
415 });
416 dom.window.close();
417 }
418
419 {
420 const pending = new Map<string, (detail: WorkspaceChangeDetailView) => void>();
421 const workspaceChangeDetail = (_tabID: string, path: string): Promise<WorkspaceChangeDetailView> =>
422 new Promise((resolve) => pending.set(path, resolve));
423 const { dom, root, rerender } = await renderFilesWorkspace(
424 {
425 WorkspaceChanges: async () => ({
426 files: [
427 { path: "session-a.ts", sources: ["session"] },
428 { path: "session-b.ts", sources: ["session"] },
429 ],
430 gitAvailable: true,
431 }),
432 WorkspaceChangeDetail: workspaceChangeDetail,
433 },
434 {
435 tabId: "shared-tab",
436 cwd: "/repo",
437 workspaceScopeKey: "session-a",
438 initialViewMode: "changed",
439 changeRevealRequest: { id: 1, path: "session-a.ts" },
440 },
441 );
442
443 await waitFor("session A change detail request", () => pending.has("session-a.ts"));
444 await rerender({ workspaceScopeKey: "session-b", changeRevealRequest: { id: 2, path: "session-b.ts" } });
445 await waitFor("session B change detail request", () => pending.has("session-b.ts"));
446
447 await act(async () => {
448 pending.get("session-b.ts")?.({
449 source: "session",
450 added: 1,
451 diff: "--- a/session-b.ts\n+++ b/session-b.ts\n@@ -1 +1 @@\n-old-b\n+current-b",
452 });
453 await flushPromises();
454 });
455 await waitFor("current session B detail", () => document.body.textContent?.includes("current-b") === true);
456
457 await act(async () => {
458 pending.get("session-a.ts")?.({
459 source: "session",
460 added: 1,
461 diff: "--- a/session-a.ts\n+++ b/session-a.ts\n@@ -1 +1 @@\n-old-a\n+stale-a",
462 });
463 await flushPromises();
464 });
465 ok(document.body.textContent?.includes("current-b") === true, "same-tab session switch keeps the current change detail");
466 ok(document.body.textContent?.includes("stale-a") === false, "late change detail cannot overwrite the current session");
467
468 await act(async () => {
469 root.unmount();
470 });
471 dom.window.close();
472 }
473
474 {
475 // A keyboard tab switch fires no mousedown/scroll/Escape, so floating menus
476 // that captured the previous scope's text/paths must be discarded when the
477 // tab/scope changes — otherwise Add to Chat would route the old scope's
478 // selection into the newly active session.
479 const { dom, root, rerender } = await renderFilesWorkspace(
480 {
481 ListDirForTab: async () => [{ name: "app.ts", isDir: false }],
482 ReadFileForTab: async () => ({
483 path: "app.ts",
484 body: "const value = 1;",
485 size: 16,
486 truncated: false,
487 binary: false,
488 }),
489 },
490 );
491 await act(async () => {
492 await performResourceAction({ source: "workspace", hostId: "local", tabId: "tab-a", path: "app.ts" }, "preview");
493 await flushPromises();
494 });
495
496 await waitFor("code preview", () => document.body.textContent?.includes("const value = 1;") === true);
497 const previewBody = document.querySelector(".workspace-preview__body") as HTMLElement;
498 const textNode = document.createTreeWalker(previewBody, 4 /* NodeFilter.SHOW_TEXT */).nextNode();
499 if (!textNode) throw new Error("preview rendered no text node to select");
500 const range = document.createRange();
501 range.selectNodeContents(textNode);
502 const selection = document.getSelection();
503 selection?.removeAllRanges();
504 selection?.addRange(range);
505 await act(async () => {
506 previewBody.dispatchEvent(new window.MouseEvent("mouseup", { bubbles: true, clientX: 60, clientY: 60 }));
507 await flushPromises();
508 });
509 ok(document.querySelector(".floating-menu") != null, "selecting preview code pops the Add to Chat toolbar");
510 await rerender({ tabId: "tab-b" });
511 ok(document.querySelector(".floating-menu") == null, "a tab switch discards the selection toolbar");
512
513 const tree = document.querySelector(".workspace-tree") as HTMLElement;
514 await act(async () => {
515 tree.dispatchEvent(new window.MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 30, clientY: 200 }));
516 await flushPromises();
517 });
518 ok(document.querySelector(".context-menu") != null, "right-clicking blank tree space opens the tree menu");
519 await rerender({ workspaceScopeKey: "scope-b" });
520 ok(document.querySelector(".context-menu") == null, "a scope switch discards the tree menu");
521
522 await act(async () => {
523 root.unmount();
524 });
525 dom.window.close();
526 }
527
528 {
529 const { dom, root, rerender } = await renderFilesWorkspace(
530 {
531 ListDirForTab: async (_tabId, dir) => {
532 if (dir === "") {
533 return [
534 { name: "alpha", isDir: true },
535 { name: "beta", isDir: true },
536 ];
537 }
538 if (dir === "alpha/") {
539 return [
540 { name: "nested", isDir: true },
541 { name: "alpha.txt", isDir: false },
542 ];
543 }
544 if (dir === "alpha/nested/") return [{ name: "deep.ts", isDir: false }];
545 if (dir === "beta/") return [{ name: "beta.txt", isDir: false }];
546 return [];
547 },
548 },
549 {
550 workspaceScopeKey: "scope-a",
551 workspaceMemoryKey: "session-a",
552 workspaceMemoryVisitId: 1,
553 },
554 );
555
556 const clickPath = async (path: string) => {
557 const row = document.querySelector<HTMLButtonElement>(`[data-workspace-path="${path}"]`);
558 if (!row) throw new Error(`missing workspace row ${path}`);
559 await act(async () => {
560 row.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
561 await flushPromises();
562 });
563 };
564
565 await waitFor("session A roots", () => document.querySelector('[data-workspace-path="alpha/"]') != null);
566 await clickPath("alpha/");
567 await waitFor("session A nested directory", () => document.querySelector('[data-workspace-path="alpha/nested/"]') != null);
568 await clickPath("alpha/nested/");
569 await waitFor("session A deep file", () => document.querySelector('[data-workspace-path="alpha/nested/deep.ts"]') != null);
570 await clickPath("beta/");
571 await waitFor("session A beta file", () => document.querySelector('[data-workspace-path="beta/beta.txt"]') != null);
572
573 await rerender({ initialViewMode: "changed" });
574 await rerender({ initialViewMode: "files" });
575 await waitFor("same-session restored tree", () => document.querySelector('[data-workspace-path="alpha/nested/deep.ts"]') != null);
576 ok(
577 document.querySelector('[data-workspace-path="beta/beta.txt"]') != null,
578 "Files → Changes → Files preserves the exact expanded tree in one session",
579 );
580
581 await rerender({
582 workspaceScopeKey: "scope-b",
583 workspaceMemoryKey: "session-b",
584 workspaceMemoryVisitId: 2,
585 });
586 await waitFor("session B roots", () => document.querySelector('[data-workspace-path="alpha/"]') != null);
587 await rerender({
588 workspaceScopeKey: "scope-a-returned",
589 workspaceMemoryKey: "session-a",
590 workspaceMemoryVisitId: 3,
591 });
592 await waitFor("returned session A roots", () => document.querySelector('[data-workspace-path="alpha/"]') != null);
593 ok(
594 document.querySelector('[data-workspace-path="alpha/nested/"]') == null &&
595 document.querySelector('[data-workspace-path="beta/beta.txt"]') == null,
596 "returning to a session presents every remembered root collapsed",
597 );
598
599 await clickPath("alpha/");
600 await waitFor("restored alpha subtree", () => document.querySelector('[data-workspace-path="alpha/nested/deep.ts"]') != null);
601 ok(
602 document.querySelector('[data-workspace-path="beta/beta.txt"]') == null,
603 "opening one returned root restores only that root's remembered subtree",
604 );
605
606 await act(async () => {
607 root.unmount();
608 });
609 dom.window.close();
610 }
611
612 {
613 const { dom, root } = await renderFilesWorkspace({
614 ListDirForTab: async (_tabId, dir) => {
615 if (dir === "") return [{ name: "src", isDir: true }];
616 if (dir === "src/") return [{ name: "main", isDir: true }];
617 if (dir === "src/main/") return [{ name: "java", isDir: true }];
618 if (dir === "src/main/java/") return [{ name: "App.java", isDir: false }];
619 return [];
620 },
621 });
622
623 await waitFor("compacted directory chain", () =>
624 document.querySelector('[data-workspace-path="src/main/java/"]')?.textContent?.includes("src / main / java") === true,
625 );
626 ok(
627 document.querySelectorAll('[data-workspace-path="src/main/java/"]').length === 1 &&
628 document.querySelector('[data-workspace-path="src/"]') == null,
629 "single-child directory chains render as one compact folder row",
630 );
631
632 await act(async () => {
633 document
634 .querySelector<HTMLButtonElement>('[data-workspace-path="src/main/java/"]')
635 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
636 await flushPromises();
637 });
638 await waitFor("compact directory child", () => document.querySelector('[data-workspace-path="src/main/java/App.java"]') != null);
639 ok(
640 document.querySelectorAll('[data-workspace-path="src/main/java/App.java"] .workspace-tree__guide').length === 1,
641 "nested file rows render one guide for each visible ancestor level",
642 );
643 ok(
644 document.querySelector('[data-workspace-path="src/main/java/App.java"] .workspace-file-icon')?.textContent !== "",
645 "workspace files render a Seti file-type icon",
646 );
647
648 await act(async () => {
649 root.unmount();
650 });
651 dom.window.close();
652 }
653
654 {
655 let resolveCodePreview!: (preview: FilePreview) => void;
656 const codePreview = new Promise<FilePreview>((resolve) => { resolveCodePreview = resolve; });
657 const { dom, root } = await renderFilesWorkspace({
658 ListDirForTab: async (_tabId, dir) => dir === ""
659 ? [
660 { name: "code.ts", isDir: false },
661 { name: "README.md", isDir: false },
662 ]
663 : [],
664 ReadFileForTab: async (_tabId, path) => path === "README.md"
665 ? { path, body: "# Documentation", size: 15, truncated: false, binary: false }
666 : codePreview,
667 });
668
669 await waitFor("searchable code file", () => document.querySelector('[data-workspace-path="code.ts"]') != null);
670 await act(async () => {
671 document
672 .querySelector<HTMLButtonElement>('[data-workspace-path="code.ts"]')
673 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
674 await flushPromises();
675 });
676 await waitFor("pending code preview layout", () => document.querySelector(".workspace-preview__body--code") != null);
677 ok(document.querySelector(".workspace-preview__body--code") != null, "code preview uses its final layout while loading");
678 await act(async () => {
679 resolveCodePreview({ path: "code.ts", body: "const value = 42;", size: 17, truncated: false, binary: false });
680 await flushPromises();
681 });
682 await waitFor("code preview search action", () => document.querySelector('button[aria-label="Find"]') != null);
683 ok(document.querySelector(".workspace-preview__body--code") != null, "resolved code preview keeps the loading layout");
684 ok(document.querySelector('button[aria-label="Find"]') != null, "searchable code previews expose a visible search action");
685
686 const filterInput = document.querySelector<HTMLInputElement>('input[placeholder="Filter files…"]');
687 await act(async () => {
688 filterInput?.focus();
689 filterInput?.dispatchEvent(new window.KeyboardEvent("keydown", {
690 key: "f",
691 ctrlKey: true,
692 bubbles: true,
693 cancelable: true,
694 }));
695 await flushPromises();
696 });
697 await waitFor("panel-scoped code search", () => document.querySelector(".code-search__input") != null);
698 ok(document.activeElement === document.querySelector(".code-search__input"), "workspace find shortcut opens and focuses code search from the file filter");
699
700 await act(async () => {
701 document
702 .querySelector<HTMLButtonElement>(".code-search__close")
703 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
704 await flushPromises();
705 });
706 await waitFor("closed code search", () => document.querySelector(".code-search") == null);
707 await act(async () => {
708 document
709 .querySelector<HTMLButtonElement>('button[aria-label="Find"]')
710 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
711 await flushPromises();
712 });
713 await waitFor("button-opened code search", () => document.querySelector(".code-search__input") != null);
714 ok(document.activeElement === document.querySelector(".code-search__input"), "visible search action opens and focuses the same search UI");
715
716 await act(async () => {
717 document
718 .querySelector<HTMLButtonElement>('[data-workspace-path="README.md"]')
719 ?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
720 await flushPromises();
721 });
722 await waitFor("markdown preview", () => document.body.textContent?.includes("Documentation") === true);
723 ok(document.querySelector('button[aria-label="Find"]') == null, "Markdown previews do not expose the code-search action");
724 const markdownFindEvent = new window.KeyboardEvent("keydown", {
725 key: "f",
726 ctrlKey: true,
727 bubbles: true,
728 cancelable: true,
729 });
730 filterInput?.dispatchEvent(markdownFindEvent);
731 ok(!markdownFindEvent.defaultPrevented, "Markdown previews preserve the host find shortcut");
732
733 await act(async () => {
734 root.unmount();
735 });
736 dom.window.close();
737 }
738
739 {
740 const javaIcon = workspaceFileIcon("App.java");
741 const markdownIcon = workspaceFileIcon("README.md");
742 const mavenIcon = workspaceFileIcon("pom.xml");
743 const xmlIcon = workspaceFileIcon("layout.xml");
744 ok(javaIcon.glyph !== "" && javaIcon.glyph !== markdownIcon.glyph, "Seti icons distinguish common file extensions");
745 ok(mavenIcon.glyph !== xmlIcon.glyph, "Seti exact-name mappings take precedence over generic extensions");
746 }
747
748 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
749 if (failed > 0) process.exit(1);
750
750 lines Plain Text