返回 DeepSeek-Reasonix
context-window-ring.test.tsx
根目录 / desktop / frontend / src / __tests__ / context-window-ring.test.tsx
1 // Run: tsx src/__tests__/context-window-ring.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 { ContextWindowRing } from "../components/ContextWindowRing";
8 import { LocaleProvider } from "../lib/i18n";
9 import type { ContextPanelInfo } 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 eq(actual: unknown, expected: unknown, label: string) {
25 if (actual === expected) ok(true, label);
26 else ok(false, `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
27 }
28
29 function wait(ms = 0): Promise<void> {
30 return new Promise((resolve) => setTimeout(resolve, ms));
31 }
32
33 class TestResizeObserver {
34 observe() {}
35 unobserve() {}
36 disconnect() {}
37 }
38
39 function installDom() {
40 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
41 pretendToBeVisual: true,
42 url: "http://localhost/",
43 });
44 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
45 globalThis.window = dom.window as unknown as Window & typeof globalThis;
46 globalThis.document = dom.window.document;
47 globalThis.Node = dom.window.Node;
48 globalThis.HTMLElement = dom.window.HTMLElement;
49 globalThis.Event = dom.window.Event;
50 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
51 globalThis.MouseEvent = dom.window.MouseEvent;
52 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
53 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
54 globalThis.ResizeObserver = TestResizeObserver;
55 Object.defineProperty(window, "matchMedia", {
56 configurable: true,
57 value: () => ({
58 matches: true,
59 media: "(prefers-reduced-motion: reduce)",
60 onchange: null,
61 addEventListener() {},
62 removeEventListener() {},
63 addListener() {},
64 removeListener() {},
65 dispatchEvent: () => false,
66 }),
67 });
68 return dom;
69 }
70
71 function contextPanelInfo(requestCount: number): ContextPanelInfo {
72 return {
73 usedTokens: 0,
74 windowTokens: 0,
75 promptTokens: 0,
76 completionTokens: 0,
77 totalTokens: 0,
78 reasoningTokens: 0,
79 cacheHitTokens: 0,
80 cacheMissTokens: 0,
81 sessionCacheHitTokens: 0,
82 sessionCacheMissTokens: 0,
83 sessionCompletionTokens: 0,
84 requestCount,
85 elapsedMs: 0,
86 sessionCost: 0,
87 sessionCurrency: "",
88 readFiles: [],
89 changedFiles: [],
90 };
91 }
92
93 function installContextPanelMock(fn: (tabId: string) => Promise<ContextPanelInfo>) {
94 (window as unknown as { go: { main: { App: { ContextPanel: typeof fn } } } }).go = {
95 main: {
96 App: {
97 ContextPanel: fn,
98 },
99 },
100 };
101 }
102
103 async function renderRing(props: Partial<Parameters<typeof ContextWindowRing>[0]> = {}) {
104 const rootEl = document.getElementById("root");
105 if (!rootEl) throw new Error("missing root");
106 const root = createRoot(rootEl);
107 let currentProps: Parameters<typeof ContextWindowRing>[0] = {
108 enabled: true,
109 tabId: "tab-a",
110 context: { used: 10, window: 100, compactRatio: 0.8 },
111 ...props,
112 };
113 const paint = async (nextProps: Partial<Parameters<typeof ContextWindowRing>[0]> = {}) => {
114 currentProps = { ...currentProps, ...nextProps };
115 await act(async () => {
116 root.render(
117 <LocaleProvider>
118 <ContextWindowRing {...currentProps} />
119 </LocaleProvider>,
120 );
121 await wait();
122 });
123 };
124 await paint();
125 return { root, rerender: paint };
126 }
127
128 console.log("\ncontext window ring");
129
130 {
131 const dom = installDom();
132 const calls: string[] = [];
133 installContextPanelMock(async (tabId) => {
134 calls.push(tabId);
135 return contextPanelInfo(1);
136 });
137
138 const { root } = await renderRing({ enabled: false });
139
140 eq(document.querySelector(".context-ring"), null, "disabled ring renders nothing");
141 eq(calls.length, 0, "disabled ring does not request context panel data");
142
143 await act(async () => {
144 root.unmount();
145 });
146 dom.window.close();
147 }
148
149 {
150 const dom = installDom();
151 installContextPanelMock(async () => contextPanelInfo(0));
152
153 const { root } = await renderRing({ turnCost: 0.125, currency: "$" });
154 const button = document.querySelector(".context-ring") as HTMLButtonElement | null;
155 if (!button) throw new Error("missing context ring button");
156 await act(async () => {
157 button.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: null }));
158 await wait(220);
159 });
160 const turnCostRow = [...document.querySelectorAll(".context-ring-popover__row")]
161 .find((row) => row.querySelector(".context-ring-popover__label")?.textContent === "turn cost");
162 eq(
163 turnCostRow?.querySelector(".context-ring-popover__value")?.textContent,
164 "$0.1250",
165 "turn cost uses the session currency before panel info is available",
166 );
167
168 await act(async () => {
169 root.unmount();
170 });
171 dom.window.close();
172 }
173
174 {
175 const dom = installDom();
176 const calls: string[] = [];
177 const resolvers = new Map<string, (value: ContextPanelInfo) => void>();
178 installContextPanelMock((tabId) => {
179 calls.push(tabId);
180 return new Promise<ContextPanelInfo>((resolve) => {
181 resolvers.set(tabId, resolve);
182 });
183 });
184
185 const { root, rerender } = await renderRing({ tabId: "old-tab" });
186 const button = document.querySelector(".context-ring") as HTMLButtonElement | null;
187 if (!button) throw new Error("missing context ring button");
188 await act(async () => {
189 button.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: null }));
190 await wait();
191 });
192
193 await rerender({ tabId: "new-tab" });
194 const nextButton = document.querySelector(".context-ring") as HTMLButtonElement | null;
195 if (!nextButton) throw new Error("missing context ring button after tab switch");
196 await act(async () => {
197 nextButton.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: null }));
198 await wait();
199 });
200
201 await act(async () => {
202 resolvers.get("new-tab")?.(contextPanelInfo(2));
203 await wait();
204 });
205 await act(async () => {
206 resolvers.get("old-tab")?.(contextPanelInfo(1));
207 await wait();
208 });
209 await act(async () => {
210 nextButton.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: null }));
211 await wait(220);
212 });
213
214 eq(calls[0], "old-tab", "old tab request starts first");
215 eq(calls[1], "new-tab", "new tab request starts after tab switch");
216 const requestRow = [...document.querySelectorAll(".context-ring-popover__row")]
217 .find((row) => row.querySelector(".context-ring-popover__label")?.textContent === "Requests");
218 eq(
219 requestRow?.querySelector(".context-ring-popover__value")?.textContent,
220 "2",
221 "stale old-tab response cannot overwrite the new tab info",
222 );
223
224 await act(async () => {
225 root.unmount();
226 });
227 dom.window.close();
228 }
229
230 console.log(`\n${passed} passed, ${failed} failed`);
231 if (failed > 0) process.exit(1);
232
232 lines Plain Text