返回 DeepSeek-Reasonix
project-tree-window.test.ts
根目录 / desktop / frontend / src / __tests__ / project-tree-window.test.ts
1 // Run: npx tsx src/__tests__/project-tree-window.test.ts
2 import assert from "node:assert/strict";
3 import {
4 PROJECT_TREE_WINDOW_INITIAL,
5 PROJECT_TREE_WINDOW_STEP,
6 createProjectTreeRequestLimiter,
7 forgetProjectTreeWindowLimits,
8 loadProjectTreePageWindow,
9 projectTreeListKey,
10 projectTreeKnownGroupIDs,
11 projectTreeProjectsNeedingInitialLoad,
12 projectTreeRuntimeWindowLimits,
13 projectTreeWindowProjection,
14 projectTreeWindowRows,
15 reloadProjectTreeTopicLists,
16 rememberProjectTreeWindowLimit,
17 resetProjectTreeRuntimeWindowLimits,
18 } from "../lib/projectTreeWindow";
19 import type { ProjectNode } from "../lib/types";
20
21 function topics(count: number): ProjectNode[] {
22 return Array.from({ length: count }, (_, index) => ({
23 key: `topic_${index + 1}`,
24 kind: "topic",
25 label: `Topic ${index + 1}`,
26 topicId: `topic-${index + 1}`,
27 children: [],
28 }));
29 }
30
31 for (const count of [0, 1, 5, 6, 15, 16]) {
32 const rows = topics(count);
33 assert.equal(
34 projectTreeWindowRows(rows, PROJECT_TREE_WINDOW_INITIAL, () => false).length,
35 Math.min(count, PROJECT_TREE_WINDOW_INITIAL),
36 `default window for ${count}`,
37 );
38 }
39
40 const many = topics(30);
41 assert.deepEqual(
42 [
43 PROJECT_TREE_WINDOW_INITIAL,
44 PROJECT_TREE_WINDOW_INITIAL + PROJECT_TREE_WINDOW_STEP,
45 PROJECT_TREE_WINDOW_INITIAL + PROJECT_TREE_WINDOW_STEP * 2,
46 ].map((limit) => projectTreeWindowRows(many, limit, () => false).length),
47 [5, 10, 15],
48 );
49
50 const activeOutside = projectTreeWindowProjection(many, 5, (node) => node.topicId === "topic-30");
51 assert.equal(activeOutside.rows.length, 6);
52 assert.equal(activeOutside.rows[activeOutside.rows.length - 1]?.topicId, "topic-30");
53 assert.equal(new Set(activeOutside.rows.map((node) => node.key)).size, activeOutside.rows.length);
54 assert.equal(activeOutside.hasHiddenLoadedRows, true);
55
56 const activeOnlyOverflow = projectTreeWindowProjection(topics(6), 5, (node) => node.topicId === "topic-6");
57 assert.equal(activeOnlyOverflow.rows.length, 6);
58 assert.equal(activeOnlyOverflow.hasHiddenLoadedRows, false, "an active fallback row alone does not imply hidden loaded rows");
59
60 const activeInside = projectTreeWindowRows(many, 5, (node) => node.topicId === "topic-3");
61 assert.equal(activeInside.length, 5);
62 assert.equal(activeInside.filter((node) => node.topicId === "topic-3").length, 1);
63
64 assert.notEqual(projectTreeListKey("project", "group-a"), projectTreeListKey("project", "group-b"));
65 assert.notEqual(projectTreeListKey("project", "", "needle"), projectTreeListKey("project", ""));
66 assert.equal(projectTreeListKey("project", "", " Needle "), projectTreeListKey("project", "", "needle"));
67
68 const knownGroupStates = {
69 [projectTreeListKey("project", "bugs")]: { loading: false, initialized: true },
70 [projectTreeListKey("project", "feature")]: { loading: false, initialized: true },
71 [projectTreeListKey("project", "", "needle")]: { loading: false, initialized: true },
72 [projectTreeListKey("other", "ignored")]: { loading: false, initialized: true },
73 };
74 assert.deepEqual(projectTreeKnownGroupIDs(knownGroupStates, "project"), ["bugs", "feature"]);
75
76 const cachedProjects = [
77 { key: "project-a", kind: "project", label: "A", children: [] },
78 { key: "project-b", kind: "project", label: "B", children: [] },
79 ] satisfies ProjectNode[];
80 const cachedPageStates = {
81 [projectTreeListKey("project-a")]: { loading: false, initialized: true, itemKeys: ["a-1", "a-2"] },
82 [projectTreeListKey("project-b")]: { loading: false, initialized: true, itemKeys: ["b-1", "b-2"] },
83 };
84 const scheduledLoads: string[] = [];
85 const scheduleInitialLoads = (expandedKeys: ReadonlySet<string>, pageStates = cachedPageStates) => {
86 for (const project of projectTreeProjectsNeedingInitialLoad(cachedProjects, expandedKeys, "", pageStates, (item) => item.key)) {
87 scheduledLoads.push(project.key);
88 }
89 };
90 scheduleInitialLoads(new Set(["project-b"]));
91 assert.deepEqual(scheduledLoads, [], "collapsing one project does not reload an initialized sibling");
92 scheduleInitialLoads(new Set(["project-a", "project-b"]));
93 assert.deepEqual(scheduledLoads, [], "reopening one project or restoring all projects reuses initialized pages");
94 scheduleInitialLoads(new Set(["project-a", "project-b"]), {
95 ...cachedPageStates,
96 [projectTreeListKey("project-a")]: { ...cachedPageStates[projectTreeListKey("project-a")], initialized: false },
97 });
98 assert.deepEqual(scheduledLoads, ["project-a"], "only an explicitly invalidated expanded project requests its first page");
99
100 const reloadCalls: string[] = [];
101 const reloadProject = { key: "project", kind: "project", label: "Project", children: [] } satisfies ProjectNode;
102 await reloadProjectTreeTopicLists(reloadProject, "", knownGroupStates, async (_project, groupID) => {
103 reloadCalls.push(groupID || "ungrouped");
104 });
105 assert.deepEqual(reloadCalls, ["ungrouped", "bugs", "feature"], "ordinary refresh replenishes every known group list");
106 reloadCalls.length = 0;
107 await reloadProjectTreeTopicLists(reloadProject, "needle", knownGroupStates, async (_project, groupID) => {
108 reloadCalls.push(groupID || "search");
109 });
110 assert.deepEqual(reloadCalls, ["search"], "search refresh reloads only the project search list");
111
112 const limiter = createProjectTreeRequestLimiter(4);
113 let running = 0;
114 let peak = 0;
115 const releases: Array<() => void> = [];
116 const tasks = Array.from({ length: 8 }, () => limiter.run(() => new Promise<void>((resolve) => {
117 running += 1;
118 peak = Math.max(peak, running);
119 releases.push(() => {
120 running -= 1;
121 resolve();
122 });
123 })));
124 await Promise.resolve();
125 assert.equal(peak, 4, "sidebar pagination is capped at four concurrent requests");
126 while (releases.length > 0) {
127 releases.shift()?.();
128 await Promise.resolve();
129 }
130 await Promise.all(tasks);
131
132 const pageCalls: Array<{ cursor: string; limit: number }> = [];
133 const pagedTopics = topics(250);
134 const restored = await loadProjectTreePageWindow("", 205, async (cursor, limit) => {
135 pageCalls.push({ cursor, limit });
136 const start = cursor ? Number(cursor) : 0;
137 const end = Math.min(start + limit, 250);
138 return {
139 items: pagedTopics.slice(start, end),
140 nextCursor: end < 250 ? String(end) : undefined,
141 revision: pageCalls.length,
142 complete: true,
143 };
144 });
145 assert.deepEqual(pageCalls, [{ cursor: "", limit: 200 }, { cursor: "200", limit: 5 }]);
146 assert.equal(restored.items.length, 205, "refresh refills the complete expanded quota");
147 assert.equal(restored.nextCursor, "205");
148 assert.equal(restored.revision, 2);
149
150 const rememberedKey = projectTreeListKey("remembered-project", "feature");
151 const siblingKey = projectTreeListKey("sibling-project", "feature");
152 rememberProjectTreeWindowLimit(rememberedKey, 15);
153 rememberProjectTreeWindowLimit(siblingKey, 10);
154 assert.equal(projectTreeRuntimeWindowLimits()[rememberedKey], 15, "expanded quota survives a component remount");
155 resetProjectTreeRuntimeWindowLimits("remembered-project");
156 assert.equal(projectTreeRuntimeWindowLimits()[rememberedKey], undefined, "collapsing one project resets all of its window quotas");
157 assert.equal(projectTreeRuntimeWindowLimits()[siblingKey], 10, "collapsing one project preserves sibling quotas");
158 resetProjectTreeRuntimeWindowLimits();
159 assert.deepEqual(projectTreeRuntimeWindowLimits(), {}, "collapsing the whole tree resets every window quota");
160 rememberProjectTreeWindowLimit(rememberedKey, 15);
161 forgetProjectTreeWindowLimits(new Set(["other-project"]));
162 assert.equal(projectTreeRuntimeWindowLimits()[rememberedKey], undefined, "deleted projects release runtime quota state");
163
164 console.log(" PASS project tree window contract");
165
165 lines TYPESCRIPT