返回 DeepSeek-Reasonix
project-tree-organization-group-drop.test.tsx
根目录 / desktop / frontend / src / __tests__ / project-tree-organization-group-drop.test.tsx
1 // Run: tsx src/__tests__/project-tree-organization-group-drop.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 { useProjectTreeOrganization } from "../components/ProjectTreeOrganization";
8 import type { ProjectNode, ProjectTreeOrganizationBindings, SessionGroup } from "../lib/types";
9 import type { SessionOrganizationMutation, SessionOrganizationSnapshot } from "../generated/desktopContract.generated";
10 import { ToastProvider } from "../lib/toast";
11 import { LocaleProvider } from "../lib/i18n";
12
13 let passed = 0;
14 let failed = 0;
15
16 function ok(value: boolean, label: string) {
17 process.stdout.write(` ${value ? "PASS" : "FAIL"} ${label}\n`);
18 if (value) passed += 1; else failed += 1;
19 }
20
21 function installDom() {
22 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
23 pretendToBeVisual: true,
24 url: "http://localhost/",
25 });
26 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
27 globalThis.window = dom.window as unknown as Window & typeof globalThis;
28 globalThis.document = dom.window.document;
29 globalThis.Node = dom.window.Node;
30 globalThis.Element = dom.window.Element;
31 globalThis.HTMLElement = dom.window.HTMLElement;
32 globalThis.Event = dom.window.Event;
33 globalThis.MouseEvent = dom.window.MouseEvent;
34 return dom;
35 }
36
37 function topic(id: string): ProjectNode {
38 return { key: `topic-${id}`, kind: "topic", label: id, root: "/repo", topicId: id, session: { hostId: "local", sessionId: id } } as ProjectNode;
39 }
40
41 const inside = topic("inside");
42 const sibling = topic("sibling");
43 const outside = topic("outside");
44 const loose = topic("loose");
45 const folder: ProjectNode = { key: "project-/repo", kind: "project", label: "Repo", root: "/repo", children: [inside, sibling, outside, loose] };
46 const groups: SessionGroup[] = [{ id: "g1", title: "Group", sessionKeys: ["ref\0local\0inside", "ref\0local\0sibling"] }];
47
48 function Harness({ bindings }: { bindings: ProjectTreeOrganizationBindings }) {
49 const organization = useProjectTreeOrganization({ tree: [folder], refresh: async () => {}, sortMode: "created", bindings });
50 const loaded = organization.groupsFor(folder).length > 0;
51 return <>
52 <output id="loaded">{loaded ? "yes" : "no"}</output>
53 {folder.children!.map((node) => {
54 const row = organization.topicRow(node, false);
55 return <div key={node.key} id={`row-${node.topicId}`} {...row.props}>{node.label}</div>;
56 })}
57 </>;
58 }
59
60 async function flush() {
61 await new Promise((resolve) => setTimeout(resolve, 0));
62 }
63
64 function fireDrag(type: string, target: Element, data: Map<string, string>) {
65 const event = new window.Event(type, { bubbles: true, cancelable: true });
66 const dataTransfer = {
67 setData: (format: string, value: string) => { data.set(format, value); },
68 getData: (format: string) => data.get(format) ?? "",
69 effectAllowed: "all",
70 dropEffect: "none",
71 };
72 Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
73 Object.defineProperty(event, "clientY", { value: 10 });
74 target.dispatchEvent(event);
75 }
76
77 async function dropOnto(draggedID: string, targetID: string): Promise<SessionOrganizationMutation[]> {
78 const dom = installDom();
79 const mutations: SessionOrganizationMutation[] = [];
80 const snapshot = (): SessionOrganizationSnapshot => ({ groups: structuredClone(groups), revision: 1, applied: true, order: [], manualOrderEnabled: false });
81 const bindings: ProjectTreeOrganizationBindings = {
82 ReorderTopics: async () => {},
83 ListProjectGroups: async () => [],
84 SaveSessionGroups: async () => {},
85 GetSessionOrganization: async () => snapshot(),
86 UpdateSessionOrganization: async (_workspace, _revision, mutation) => { mutations.push(mutation); return snapshot(); },
87 };
88 const root = createRoot(document.getElementById("root")!);
89 await act(async () => {
90 root.render(<LocaleProvider><ToastProvider><Harness bindings={bindings} /></ToastProvider></LocaleProvider>);
91 await flush();
92 });
93 for (let attempt = 0; attempt < 30 && document.getElementById("loaded")?.textContent !== "yes"; attempt += 1) await act(flush);
94 const data = new Map<string, string>();
95 await act(async () => { fireDrag("dragstart", document.getElementById(`row-${draggedID}`)!, data); await flush(); });
96 await act(async () => { fireDrag("dragover", document.getElementById(`row-${targetID}`)!, data); await flush(); });
97 await act(async () => { fireDrag("drop", document.getElementById(`row-${targetID}`)!, data); await flush(); });
98 for (let attempt = 0; attempt < 10; attempt += 1) await act(flush);
99 await act(async () => root.unmount());
100 dom.window.close();
101 return mutations;
102 }
103
104 console.log("\nproject tree drop onto a group member row");
105
106 {
107 const mutations = await dropOnto("outside", "inside");
108 ok(mutations.length === 1 && mutations[0].kind === "set-group" && mutations[0].groupId === "g1",
109 `dropping a non-member on a member row adds it to that group (got ${JSON.stringify(mutations.map((m) => [m.kind, m.groupId ?? ""]))})`);
110 }
111
112 {
113 const mutations = await dropOnto("sibling", "inside");
114 ok(mutations.length === 1 && mutations[0].kind === "move",
115 `dropping a member on another member of its group reorders (got ${JSON.stringify(mutations.map((m) => m.kind))})`);
116 }
117
118 {
119 const mutations = await dropOnto("outside", "loose");
120 ok(mutations.length === 1 && mutations[0].kind === "move",
121 `dropping on an ungrouped row reorders (got ${JSON.stringify(mutations.map((m) => m.kind))})`);
122 }
123
124 {
125 const mutations = await dropOnto("outside", "loose");
126 ok(mutations.length === 1 && mutations[0].sortMode === "created",
127 `a move carries the sort order on screen, so a first manual move starts from it (got ${JSON.stringify(mutations.map((m) => m.sortMode ?? ""))})`);
128 }
129
130 process.stdout.write(`\nproject-tree-organization-group-drop: ${passed} passed, ${failed} failed\n`);
131 if (failed > 0) process.exit(1);
132
132 lines Plain Text