// @effect-diagnostics nodeBuiltinImport:off - reads the installed native JS bundle for synchronous VM tests. import * as NodeFS from "node:fs"; import * as NodeVM from "node:vm"; import { describe, expect, it, vi } from "vite-plus/test"; // Exercise the shipped patch without loading React Native in the Node runner. // Native scroll delivery and animation frames advance independently here. function createList(bundle: string) { const source = NodeFS.readFileSync( new URL(`../apps/mobile/node_modules/@legendapp/list/${bundle}`, import.meta.url), "utf8", ); const renderState = source.slice( source.indexOf("function setInitialRenderState("), source.indexOf("// src/core/finishInitialScroll.ts"), ); const watchdog = source.slice( source.indexOf("var INSET_END_SETTLE_WATCHDOG_FRAMES"), source.indexOf("function dispatchInitialScroll("), ); const frames: Array<() => void> = []; const values = new Map(); const scrollTo = vi.fn(); const onLoad = vi.fn(); const state = { props: { data: ["message"], onLoad, drawDistance: 500 }, initialScroll: { index: 0, viewPosition: 1 }, loadStartTime: 0, didContainersLayout: true, didFinishInitialScroll: true, didLoad: false, didUserDrag: false, scrollLength: 800, scroll: 400, lastNativeScroll: 200 as number | undefined, scrollingTo: undefined as { offset: number } | undefined, maintainingScrollAtEnd: false, refScroller: { current: { scrollTo } }, }; const ctx = { state }; let contentSize = 1200; let inset = 100; const api = NodeVM.runInNewContext( `${renderState}\n${watchdog}\n({ start: startInsetEndSettleWatchdog, complete: setInitialRenderState })`, { requestAnimationFrame: (callback: () => void) => frames.push(callback), getContentSize: () => contentSize, getContentInsetStartAdjustment: () => inset, peek$: (_ctx: unknown, key: string) => values.get(key), set$: (_ctx: unknown, key: string, value: unknown) => values.set(key, value), setAdaptiveRender: vi.fn(), scheduleFullDrawDistancePrewarm: vi.fn(), INITIAL_DRAW_DISTANCE: 250, }, ) as { start: (context: typeof ctx) => void; complete: (context: typeof ctx, flags: Record) => void; }; return { state, scrollTo, onLoad, start: () => api.start(ctx), complete: () => api.complete(ctx, {}), ready: () => values.get("readyToRender") === true, resize: (size: number) => { contentSize = size; }, setInset: (value: number) => { inset = value; }, advance(count: number) { for (let index = 0; index < count; index++) { const batch = frames.splice(0); for (const frame of batch) frame(); } }, }; } for (const bundle of ["react-native.js", "react-native.mjs"]) { describe(`initial inset end reveal (${bundle})`, () => { it("waits for the native offset instead of the optimistic scroll target", () => { const list = createList(bundle); list.start(); list.advance(8); expect(list.ready()).toBe(false); expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: 400 }); list.state.lastNativeScroll = 400; list.advance(7); expect(list.ready()).toBe(true); expect(list.onLoad).toHaveBeenCalledTimes(1); }); it("gates the seeded contentOffset completion path too", () => { const list = createList(bundle); list.complete(); expect(list.ready()).toBe(false); list.state.lastNativeScroll = 400; list.advance(7); expect(list.ready()).toBe(true); }); it("preserves an initial index before the last item", () => { const list = createList(bundle); list.state.props.data = ["requested message", "later message"]; list.state.scroll = 200; list.complete(); list.advance(8); expect(list.ready()).toBe(true); expect(list.scrollTo).not.toHaveBeenCalled(); }); it("does not count an in-flight scroll as stability", () => { const list = createList(bundle); list.state.lastNativeScroll = 400; list.state.scrollingTo = { offset: 400 }; list.start(); list.advance(8); expect(list.ready()).toBe(false); list.state.scrollingTo = undefined; list.state.maintainingScrollAtEnd = true; list.advance(8); expect(list.ready()).toBe(false); list.state.maintainingScrollAtEnd = false; list.advance(7); expect(list.ready()).toBe(true); }); it("waits for a stable end target even when native scrolling follows each measurement", () => { const list = createList(bundle); list.start(); for (let index = 0; index < 10; index++) { list.resize(1200 + index * 10); list.state.lastNativeScroll = 400 + index * 10; list.advance(1); } expect(list.ready()).toBe(false); list.advance(7); expect(list.ready()).toBe(true); }); it("accounts for header insets when content is shorter than the full viewport", () => { const list = createList(bundle); list.resize(780); list.state.scroll = -20; list.state.lastNativeScroll = -100; list.start(); list.advance(8); expect(list.ready()).toBe(false); expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: -20 }); list.state.lastNativeScroll = -20; list.advance(7); expect(list.ready()).toBe(true); }); it("keeps the reveal bounded when native events never arrive", () => { const list = createList(bundle); list.state.lastNativeScroll = undefined; list.start(); list.advance(8); expect(list.ready()).toBe(false); expect(list.scrollTo).not.toHaveBeenCalled(); list.advance(32); expect(list.ready()).toBe(true); }); it("releases control on drag and never starts another initial hold", () => { const list = createList(bundle); list.start(); list.state.didUserDrag = true; list.advance(1); expect(list.ready()).toBe(true); expect(list.scrollTo).not.toHaveBeenCalled(); list.complete(); list.start(); list.advance(20); expect(list.state.didUserDrag).toBe(true); expect(list.scrollTo).not.toHaveBeenCalled(); expect(list.onLoad).toHaveBeenCalledTimes(1); }); it("preserves a drag that started before initial layout completed", () => { const list = createList(bundle); list.state.didUserDrag = true; list.complete(); list.advance(1); expect(list.ready()).toBe(true); expect(list.state.didUserDrag).toBe(true); expect(list.scrollTo).not.toHaveBeenCalled(); }); it("reveals short content at UIKit's resting offset without awaiting a scroll event", () => { const list = createList(bundle); list.resize(700); list.state.lastNativeScroll = undefined; list.complete(); expect(list.ready()).toBe(true); list.advance(10); expect(list.scrollTo).not.toHaveBeenCalled(); }); it("does not gate empty lists or lists without an automatic header inset", () => { const list = createList(bundle); list.setInset(0); list.complete(); expect(list.ready()).toBe(true); const empty = createList(bundle); empty.state.props.data = []; empty.complete(); expect(empty.ready()).toBe(true); }); }); }