From aca867aa860e6f22bc9b770e1655b906c78d2d93 Mon Sep 17 00:00:00 2001 From: galuis116 Date: Tue, 21 Jul 2026 11:45:40 -0300 Subject: [PATCH] test(miner-ui): cover chat-scroll stick-to-bottom boundary and clamp (#7793) apps/loopover-miner-ui/src/lib/chat-scroll.ts encodes the 80px near-bottom threshold and the scroll clamp for the chat auto-scroll feature, but had no direct test: its only consumer (components/chat/message-list.tsx) is covered for DOM structure, and jsdom does not meaningfully simulate scrollTop / scrollHeight there. This logic already needed one dedicated bug-fix pass (#7229/#7298), which is exactly the kind of regression a unit test catches. Adds chat-scroll.test.ts following the one-test-per-pure-module convention demo-data.test.ts already establishes in the same directory: - isChatViewportNearBottom: the <= boundary at exactly CHAT_NEAR_BOTTOM_PX, one pixel past it, sitting exactly at the bottom, content shorter than the viewport (negative distance), scrolled far up, and an explicit thresholdPx overriding the default. - scrollChatViewportToBottom: scrolls to the maximum offset, clamps to 0 when the content is shorter than the viewport, and is idempotent once bottomed. Test-only, no production change. --- .../src/lib/chat-scroll.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 apps/loopover-miner-ui/src/lib/chat-scroll.test.ts diff --git a/apps/loopover-miner-ui/src/lib/chat-scroll.test.ts b/apps/loopover-miner-ui/src/lib/chat-scroll.test.ts new file mode 100644 index 0000000000..2ec1408584 --- /dev/null +++ b/apps/loopover-miner-ui/src/lib/chat-scroll.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { CHAT_NEAR_BOTTOM_PX, isChatViewportNearBottom, scrollChatViewportToBottom } from "./chat-scroll"; + +/** A minimal stand-in for the scroll viewport: jsdom can't set scrollHeight/clientHeight on a real element. */ +function viewport(scrollHeight: number, clientHeight: number, scrollTop = 0): HTMLElement { + return { scrollTop, scrollHeight, clientHeight } as unknown as HTMLElement; +} + +describe("isChatViewportNearBottom (#7229)", () => { + it("treats a distance of exactly CHAT_NEAR_BOTTOM_PX as still pinned (the <= boundary)", () => { + // scrollHeight - scrollTop - clientHeight === 80 + expect(isChatViewportNearBottom(viewport(1000, 400, 520))).toBe(true); + }); + + it("treats one pixel past the threshold as scrolled away", () => { + // ...=== 81 + expect(isChatViewportNearBottom(viewport(1000, 400, 519))).toBe(false); + }); + + it("is true when the viewport sits exactly at the bottom", () => { + expect(isChatViewportNearBottom(viewport(1000, 400, 600))).toBe(true); + }); + + it("is true for content shorter than the viewport (negative distance, nothing to scroll)", () => { + expect(isChatViewportNearBottom(viewport(200, 400, 0))).toBe(true); + }); + + it("is false when scrolled far up", () => { + expect(isChatViewportNearBottom(viewport(5000, 400, 0))).toBe(false); + }); + + it("honors an explicit thresholdPx instead of the default", () => { + // distance === 10: outside a 0px threshold, inside the 80px default. + expect(isChatViewportNearBottom(viewport(1000, 400, 590), 0)).toBe(false); + expect(isChatViewportNearBottom(viewport(1000, 400, 590))).toBe(true); + // distance === 0 still counts at a 0px threshold (<=, not <). + expect(isChatViewportNearBottom(viewport(1000, 400, 600), 0)).toBe(true); + }); + + it("exposes the documented 80px threshold", () => { + expect(CHAT_NEAR_BOTTOM_PX).toBe(80); + }); +}); + +describe("scrollChatViewportToBottom (#7229)", () => { + it("scrolls to the maximum scrollable offset", () => { + const el = viewport(1000, 400, 0); + scrollChatViewportToBottom(el); + expect(el.scrollTop).toBe(600); + }); + + it("clamps to 0 when the content is shorter than the viewport", () => { + const el = viewport(200, 400, 25); + scrollChatViewportToBottom(el); + expect(el.scrollTop).toBe(0); + }); + + it("leaves an already-bottomed viewport at the same offset (idempotent)", () => { + const el = viewport(1000, 400, 600); + scrollChatViewportToBottom(el); + expect(el.scrollTop).toBe(600); + }); +});