返回 DeepSeek-Reasonix
chat-scroll-controller.test.ts
根目录 / desktop / frontend / src / __tests__ / chat-scroll-controller.test.ts
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import { ChatScrollController } from "../lib/chatScrollController";
4
5 const dom = new JSDOM('<div id="scroll"><div id="content"></div></div>', { url: "http://localhost" });
6 Object.assign(globalThis, { window: dom.window, document: dom.window.document, HTMLElement: dom.window.HTMLElement });
7 let nextFrame = 0;
8 const frames = new Map<number, FrameRequestCallback>();
9 globalThis.requestAnimationFrame = callback => { frames.set(++nextFrame, callback); return nextFrame; };
10 globalThis.cancelAnimationFrame = id => { frames.delete(id); };
11 const callbacks: ResizeObserverCallback[] = [];
12 globalThis.ResizeObserver = class {
13 constructor(callback: ResizeObserverCallback) { callbacks.push(callback); }
14 observe() {} unobserve() {} disconnect() {}
15 } as unknown as typeof ResizeObserver;
16 const el = document.getElementById("scroll")!;
17 const content = document.getElementById("content")!;
18 let offset = 0, writes = 0;
19 Object.defineProperties(el, {
20 clientHeight: { get: () => 200 }, scrollHeight: { get: () => content.children.length * 100 },
21 scrollTop: { get: () => offset, set: value => { writes++; offset = Math.max(0, Math.min(value, content.children.length * 100 - 200)); } },
22 });
23 el.getBoundingClientRect = () => ({ top: 0, bottom: 200 }) as DOMRect;
24 function row(key: string) {
25 const node = document.createElement("div"); node.dataset.chatAnchorKey = key; node.dataset.chatTurn = key; node.textContent = key;
26 node.getBoundingClientRect = () => { const top = Array.from(content.children).indexOf(node) * 100 - offset; return { top, bottom: top + 100 } as DOMRect; };
27 return node;
28 }
29 for (let index = 0; index < 10; index++) content.append(row(`row${index}`));
30 const controller = new ChatScrollController("scroll-test");
31 controller.attach(el, content); controller.ready();
32 assert.equal(offset, 800);
33 const unchanged = writes; controller.layout(); assert.equal(writes, unchanged, "no redundant physical write");
34 el.dispatchEvent(new dom.window.WheelEvent("wheel", { deltaY: -5 }));
35 offset = 795; el.dispatchEvent(new dom.window.Event("scroll"));
36 controller.layout();
37 assert.equal(offset, 795, "small upward movement inside 24px never snaps back");
38 assert.equal(controller.getSnapshot().following, false);
39 offset = 450; el.dispatchEvent(new dom.window.Event("scroll")); controller.beforeChange();
40 const held = content.children[4].getBoundingClientRect().top;
41 content.prepend(row("older")); controller.layout();
42 assert.equal(content.children[5].getBoundingClientRect().top, held, "prepend preserves row-relative position");
43 const reading = offset; content.append(row("tail")); controller.layout();
44 assert.equal(offset, reading, "tail growth does not move a reader");
45 controller.jump("row2"); assert.equal(document.querySelector('[data-chat-anchor-key="row2"]')!.getBoundingClientRect().top, 0);
46 controller.toBottom(); assert.equal(offset, 1000);
47 const callback = callbacks[0];
48 controller.dispose();
49 const afterDispose = writes;
50 callback([], {} as ResizeObserver);
51 frames.forEach(callback => callback(16));
52 assert.equal(writes, afterDispose, "late observers and frames cannot write after disposal");
53 dom.window.close();
54 console.log("chat scroll: reader intent, prepend, growth, jump, no-op and stale observer tests passed");
55
55 lines TYPESCRIPT