import { describe, expect, it } from "vite-plus/test"; import { getAnchoredTurnMetrics, getRowBottom } from "./timelineScrollAnchoring"; function buildState({ positions, sizes, scroll = 0, scrollLength = 700, }: { readonly positions: readonly number[]; readonly sizes: readonly number[]; readonly scroll?: number; readonly scrollLength?: number; }) { return { data: positions.map((_, index) => index), scroll, scrollLength, positionAtIndex: (index: number) => positions[index], sizeAtIndex: (index: number) => sizes[index], }; } describe("timeline scroll anchoring", () => { it("measures row bottoms from LegendList row position and size", () => { const state = buildState({ positions: [0, 120], sizes: [80, 40], }); expect(getRowBottom(state, 1)).toBe(160); }); it("treats the active turn as fitting when it fits above the composer", () => { const state = buildState({ positions: [0, 300, 460], sizes: [240, 80, 140], scrollLength: 760, }); const metrics = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 180, anchorOffset: 16, }); expect(metrics?.turnHeight).toBe(300); expect(metrics?.usableViewportHeight).toBe(564); expect(metrics?.overflowsUsableViewport).toBe(false); expect(metrics?.targetScrollToRevealEnd).toBe(36); expect(metrics?.scrollDeltaToRevealEnd).toBe(36); }); it("targets the real row end instead of any temporary reserved tail", () => { const state = buildState({ positions: [0, 1720, 1880], sizes: [1600, 80, 120], scroll: 1900, scrollLength: 760, }); const metrics = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 180, anchorOffset: 16, }); expect(metrics?.lastBottom).toBe(2000); expect(metrics?.targetScrollToRevealEnd).toBe(1436); expect(metrics?.scrollDeltaToRevealEnd).toBe(0); }); it("reports overflow only for the current anchored turn", () => { const state = buildState({ positions: [0, 900, 1180], sizes: [800, 220, 300], scroll: 900, scrollLength: 760, }); const metrics = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 180, anchorOffset: 16, }); expect(metrics?.turnHeight).toBe(580); expect(metrics?.usableViewportHeight).toBe(564); expect(metrics?.overflowsUsableViewport).toBe(true); }); it("returns the minimal positive scroll delta needed to reveal the turn end", () => { const state = buildState({ positions: [0, 900, 1180], sizes: [800, 220, 360], scroll: 900, scrollLength: 760, }); const metrics = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 180, anchorOffset: 16, }); expect(metrics?.lastBottom).toBe(1540); expect(metrics?.visibleUsableBottom).toBe(1464); expect(metrics?.scrollDeltaToRevealEnd).toBe(76); }); it("subtracts composer height from usable viewport height", () => { const state = buildState({ positions: [0, 300], sizes: [120, 470], scrollLength: 700, }); const withoutComposer = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 0, anchorOffset: 16, }); const withComposer = getAnchoredTurnMetrics({ state, anchorIndex: 1, composerOverlayHeight: 220, anchorOffset: 16, }); expect(withoutComposer?.overflowsUsableViewport).toBe(false); expect(withComposer?.overflowsUsableViewport).toBe(true); }); });