mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-15 07:36:27 -05:00
118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { VirtualizerCore } from "./core";
|
|
|
|
const ESTIMATED_SIZE = 40;
|
|
const GAP = 8;
|
|
|
|
function naiveStarts(sizes: number[]) {
|
|
const starts: number[] = [];
|
|
let start = 0;
|
|
for (const size of sizes) {
|
|
starts.push(start);
|
|
start += size + GAP;
|
|
}
|
|
return starts;
|
|
}
|
|
|
|
function naiveRange(sizes: number[], scrollTop: number, viewport: number) {
|
|
const starts = naiveStarts(sizes);
|
|
const firstVisible = sizes.findIndex(
|
|
(size, i) => starts[i] + size >= scrollTop,
|
|
);
|
|
if (firstVisible === -1) return [];
|
|
let lastVisible = -1;
|
|
for (let i = 0; i < sizes.length; i++) {
|
|
if (starts[i] <= scrollTop + viewport) lastVisible = i;
|
|
}
|
|
const from = Math.max(0, firstVisible - 3);
|
|
const to = Math.min(sizes.length - 1, lastVisible + 3);
|
|
return Array.from({ length: to - from + 1 }, (_, offset) => ({
|
|
index: from + offset,
|
|
start: starts[from + offset],
|
|
}));
|
|
}
|
|
|
|
function seededSizes(count: number, seed: number) {
|
|
let state = seed;
|
|
return Array.from({ length: count }, () => {
|
|
state = (state * 1103515245 + 12345) % 2147483648;
|
|
return 20 + (state % 120);
|
|
});
|
|
}
|
|
|
|
describe("VirtualizerCore", () => {
|
|
test("offsets and total size match a full recount after out-of-order measurements", () => {
|
|
const sizes = seededSizes(50, 7);
|
|
const core = new VirtualizerCore({
|
|
count: sizes.length,
|
|
estimatedSize: ESTIMATED_SIZE,
|
|
gap: GAP,
|
|
});
|
|
|
|
for (const index of [30, 5, 49, 12, 0, 5]) {
|
|
core.measure(index, sizes[index]);
|
|
}
|
|
const applied = sizes.map((size, i) =>
|
|
[30, 5, 49, 12, 0].includes(i) ? size : ESTIMATED_SIZE,
|
|
);
|
|
const starts = naiveStarts(applied);
|
|
|
|
for (let i = 0; i < sizes.length; i++) {
|
|
expect(core.startOf(i)).toBe(starts[i]);
|
|
}
|
|
expect(core.totalSize()).toBe(starts[49] + applied[49]);
|
|
});
|
|
|
|
test.each([
|
|
{ why: "top", scrollTop: 0, viewport: 300 },
|
|
{ why: "middle", scrollTop: 1234, viewport: 300 },
|
|
{ why: "row boundary", scrollTop: 20 + GAP, viewport: 100 },
|
|
{ why: "zero viewport", scrollTop: 500, viewport: 0 },
|
|
{ why: "past the end", scrollTop: 100_000, viewport: 300 },
|
|
])("range matches a linear scan ($why)", ({ scrollTop, viewport }) => {
|
|
const sizes = seededSizes(80, 3);
|
|
const core = new VirtualizerCore({
|
|
count: sizes.length,
|
|
estimatedSize: ESTIMATED_SIZE,
|
|
gap: GAP,
|
|
});
|
|
for (const [index, size] of sizes.entries()) {
|
|
core.measure(index, size);
|
|
}
|
|
|
|
expect(core.range(scrollTop, viewport)).toEqual(
|
|
naiveRange(sizes, scrollTop, viewport),
|
|
);
|
|
});
|
|
|
|
test("shrinking the count drops stale measurements and offsets", () => {
|
|
const core = new VirtualizerCore({
|
|
count: 10,
|
|
estimatedSize: ESTIMATED_SIZE,
|
|
gap: GAP,
|
|
});
|
|
core.measure(9, 200);
|
|
expect(core.totalSize()).toBe(9 * (ESTIMATED_SIZE + GAP) + 200);
|
|
|
|
core.setCount(5);
|
|
expect(core.totalSize()).toBe(4 * (ESTIMATED_SIZE + GAP) + ESTIMATED_SIZE);
|
|
|
|
core.setCount(10);
|
|
expect(core.sizeOf(9)).toBe(ESTIMATED_SIZE);
|
|
expect(core.totalSize()).toBe(9 * (ESTIMATED_SIZE + GAP) + ESTIMATED_SIZE);
|
|
});
|
|
|
|
test("measuring a row shifts only the rows after it", () => {
|
|
const core = new VirtualizerCore({
|
|
count: 4,
|
|
estimatedSize: ESTIMATED_SIZE,
|
|
gap: GAP,
|
|
});
|
|
expect(core.measure(1, 100)).toBe(true);
|
|
expect(core.measure(1, 100)).toBe(false);
|
|
|
|
expect(core.startOf(1)).toBe(ESTIMATED_SIZE + GAP);
|
|
expect(core.startOf(2)).toBe(ESTIMATED_SIZE + GAP + 100 + GAP);
|
|
});
|
|
});
|