214 lines · 7.3 KB · TypeScript
RawDownload
// @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<string, unknown>();  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<string, boolean>) => 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);    });  });}