返回 DeepSeek-Reasonix
resize-drag.test.ts
根目录 / desktop / frontend / src / __tests__ / resize-drag.test.ts
1 // Run: tsx src/__tests__/resize-drag.test.ts
2
3 import { JSDOM } from "jsdom";
4 import { createPointerResizeLifecycle, createRafResizeUpdater } from "../lib/resizeDrag";
5
6 let passed = 0;
7 let failed = 0;
8
9 function eq(a: unknown, b: unknown, label: string) {
10 if (a === b) {
11 process.stdout.write(` PASS ${label}\n`);
12 passed += 1;
13 } else {
14 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
15 failed += 1;
16 }
17 }
18
19 console.log("\nresizeDrag");
20
21 const frames: FrameRequestCallback[] = [];
22 const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
23 const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
24 globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
25 frames.push(callback);
26 return frames.length;
27 }) as typeof requestAnimationFrame;
28 globalThis.cancelAnimationFrame = ((id: number) => {
29 frames[id - 1] = () => undefined;
30 }) as typeof cancelAnimationFrame;
31
32 try {
33 const applied: Array<[string, string]> = [];
34 const attrs: Record<string, string> = {};
35 const updater = createRafResizeUpdater({
36 target: {
37 style: {
38 setProperty(name: string, value: string) {
39 applied.push([name, value]);
40 },
41 },
42 },
43 separator: {
44 setAttribute(name: string, value: string) {
45 attrs[name] = value;
46 },
47 },
48 cssVar: "--pane-width",
49 });
50
51 updater.schedule(101.2);
52 updater.schedule(148.7);
53 eq(applied.length, 0, "live resize waits for animation frame");
54 eq(frames.length, 1, "multiple pointer moves share one animation frame");
55 frames[0](0);
56 eq(applied.length, 1, "one frame applies one live resize");
57 eq(applied[0]?.[0], "--pane-width", "live resize writes the configured CSS variable");
58 eq(applied[0]?.[1], "149px", "live resize applies the latest rounded pixel value");
59 eq(attrs["aria-valuenow"], "149", "live resize keeps separator ARIA in sync");
60
61 frames.length = 0;
62 const flushed: Array<[string, string]> = [];
63 const flushUpdater = createRafResizeUpdater({
64 target: {
65 style: {
66 setProperty(name: string, value: string) {
67 flushed.push([name, value]);
68 },
69 },
70 },
71 cssVar: "--pane-width",
72 });
73 flushUpdater.schedule(600);
74 frames[0](0);
75 flushUpdater.schedule(420);
76 eq(flushed[flushed.length - 1]?.[1], "600px", "pending final resize waits for animation frame before flush");
77 flushUpdater.flush();
78 eq(flushed[flushed.length - 1]?.[1], "420px", "flush applies the final resize before React state commits");
79
80 frames.length = 0;
81 const appliedValues: number[] = [];
82 const callbackUpdater = createRafResizeUpdater({
83 target: {
84 style: {
85 setProperty() {
86 // CSS write is covered above; this block checks the synchronized callback contract.
87 },
88 },
89 },
90 cssVar: "--pane-width",
91 onApply(value) {
92 appliedValues.push(value);
93 },
94 });
95 callbackUpdater.schedule(279.7);
96 callbackUpdater.schedule(300.2);
97 eq(appliedValues.length, 0, "live resize callback waits for animation frame");
98 frames[0](0);
99 eq(appliedValues.join(","), "300", "live resize callback receives the applied rounded value");
100
101 const dom = new JSDOM('<!doctype html><button id="separator"></button>');
102 globalThis.window = dom.window as unknown as Window & typeof globalThis;
103 const separator = dom.window.document.getElementById("separator") as HTMLButtonElement;
104 let capturedPointer: number | null = null;
105 let finished = 0;
106 let moved = 0;
107 separator.setPointerCapture = (pointerId) => { capturedPointer = pointerId; };
108 separator.hasPointerCapture = (pointerId) => capturedPointer === pointerId;
109 separator.releasePointerCapture = (pointerId) => {
110 capturedPointer = null;
111 const lost = new dom.window.Event("lostpointercapture") as Event & { pointerId: number };
112 Object.defineProperty(lost, "pointerId", { value: pointerId });
113 separator.dispatchEvent(lost);
114 };
115 const pointerEvent = (type: string, pointerId: number) => {
116 const event = new dom.window.Event(type) as Event & { pointerId: number };
117 Object.defineProperty(event, "pointerId", { value: pointerId });
118 return event;
119 };
120 const lifecycle = createPointerResizeLifecycle({
121 separator,
122 pointerId: 7,
123 onMove: () => { moved += 1; },
124 onFinish: () => { finished += 1; },
125 });
126 dom.window.dispatchEvent(pointerEvent("pointermove", 8));
127 dom.window.dispatchEvent(pointerEvent("pointermove", 7));
128 eq(moved, 1, "resize lifecycle ignores other pointers and delivers the active pointer");
129 dom.window.dispatchEvent(pointerEvent("pointercancel", 7));
130 eq(finished, 1, "pointer cancellation finishes the resize once");
131 lifecycle.finish();
132 separator.dispatchEvent(pointerEvent("lostpointercapture", 7));
133 dom.window.dispatchEvent(new dom.window.Event("blur"));
134 eq(finished, 1, "capture loss, blur, and component cleanup cannot finish a gesture twice");
135 eq(capturedPointer, null, "resize finish releases active pointer capture");
136 dom.window.close();
137 } finally {
138 globalThis.requestAnimationFrame = originalRequestAnimationFrame;
139 globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
140 }
141
142 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
143 if (failed > 0) process.exit(1);
144
144 lines TYPESCRIPT