mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
215 lines
7.3 KiB
TypeScript
215 lines
7.3 KiB
TypeScript
// @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();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
describe.each(["react-native.js", "react-native.mjs"])(
|
|
"initial inset end reveal (%s)",
|
|
(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);
|
|
});
|
|
},
|
|
);
|