import { afterEach, describe, expect, it } from "vitest"; import { ChatScrollSessionStorage } from "@/lib/chat/chat_scroll_session_storage"; import { consumeChatScrollSnapshot, getChatScrollRestoreTop, saveChatScrollSnapshot, } from "../chat-scroll-session"; afterEach(async () => { await consumeChatScrollSnapshot(); await ChatScrollSessionStorage.clearSnapshot(); document.body.innerHTML = ""; }); describe("chat scroll session", () => { it("saves image viewer scroll sessions by message anchor", async () => { const { anchorNode, scrollNode } = createScrollFixture({ anchorTop: 220, scrollTop: 120, }); saveChatScrollSnapshot(scrollNode, { anchorMessageId: "msg-image-1" }); const snapshot = await consumeChatScrollSnapshot(); expect(snapshot).toEqual({ version: 2, reason: "image-viewer", anchorMessageId: "msg-image-1", anchorOffsetTop: 120, fallbackScrollTop: 120, savedAt: expect.any(Number), }); expect(anchorNode.dataset.chatMessageId).toBe("msg-image-1"); }); it("restores scroll top by keeping the anchor at the saved visual offset", () => { const { scrollNode } = createScrollFixture({ anchorTop: 250, scrollTop: 200, }); expect( getChatScrollRestoreTop(scrollNode, { version: 2, reason: "image-viewer", anchorMessageId: "msg-image-1", anchorOffsetTop: 120, fallbackScrollTop: 120, savedAt: Date.now(), }), ).toBe(230); }); it("falls back to the saved scroll top when the anchor is missing", () => { const { scrollNode } = createScrollFixture({ anchorTop: 250, scrollTop: 200, }); expect( getChatScrollRestoreTop(scrollNode, { version: 2, reason: "image-viewer", anchorMessageId: "missing-message", anchorOffsetTop: 120, fallbackScrollTop: 180, savedAt: Date.now(), }), ).toBe(180); }); }); function createScrollFixture(input: { anchorTop: number; scrollTop: number; }): { anchorNode: HTMLElement; scrollNode: HTMLElement; } { const scrollNode = document.createElement("div"); Object.defineProperties(scrollNode, { clientHeight: { configurable: true, value: 400 }, scrollHeight: { configurable: true, value: 1000 }, scrollTop: { configurable: true, value: input.scrollTop, writable: true, }, }); scrollNode.getBoundingClientRect = () => makeRect(100); const anchorNode = document.createElement("div"); anchorNode.dataset.chatMessageId = "msg-image-1"; anchorNode.getBoundingClientRect = () => makeRect(input.anchorTop); scrollNode.appendChild(anchorNode); document.body.appendChild(scrollNode); return { anchorNode, scrollNode }; } function makeRect(top: number): DOMRect { return { bottom: top + 40, height: 40, left: 0, right: 100, toJSON: () => ({}), top, width: 100, x: 0, y: top, }; }