@shopify/flash-list
Version:
FlashList is a more performant FlatList replacement
370 lines (308 loc) • 13.8 kB
text/typescript
import {
findFirstVisibleIndex,
findLastVisibleIndex,
} from "../recyclerview/utils/findVisibleIndex";
import { RVLayout } from "../recyclerview/layout-managers/LayoutManager";
import {
createPopulatedLayoutManager,
LayoutManagerType,
getAllLayouts,
} from "./helpers/createLayoutManager";
describe("findVisibleIndex", () => {
// Helper function to create mock layouts directly for precise control
function createMockLayouts(
count: number,
startPosition: number,
itemSize: number,
isHorizontal: boolean
): RVLayout[] {
const layouts: RVLayout[] = [];
for (let i = 0; i < count; i++) {
const x = isHorizontal ? startPosition + i * itemSize : 0;
const y = isHorizontal ? 0 : startPosition + i * itemSize;
layouts.push({
x,
y,
width: isHorizontal ? itemSize : 100,
height: isHorizontal ? 100 : itemSize,
});
}
return layouts;
}
describe("findFirstVisibleIndex", () => {
// Test 1: Basic functionality - vertical layout
it("finds the first visible index in a vertical layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
20,
{ horizontal: false }
);
const layouts = getAllLayouts(layoutManager);
// Viewport starts at y=150, so the second item (index 1) should be first visible
const firstVisibleIndex = findFirstVisibleIndex(layouts, 150, false);
expect(firstVisibleIndex).toBe(1);
});
// Test 2: Basic functionality - horizontal layout
it("finds the first visible index in a horizontal layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
20,
{ horizontal: true }
);
const layouts = getAllLayouts(layoutManager);
// Viewport starts at x=150, so the second item (index 1) should be first visible
const firstVisibleIndex = findFirstVisibleIndex(layouts, 150, true);
expect(firstVisibleIndex).toBe(1);
});
// Test 3: Empty layouts array
it("returns -1 for empty layouts array", () => {
const firstVisibleIndex = findFirstVisibleIndex([], 100, false);
expect(firstVisibleIndex).toBe(-1);
});
// Test 4: All items are visible (threshold at 0)
it("returns 0 when all items are visible (threshold at 0)", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
10
);
const layouts = getAllLayouts(layoutManager);
const firstVisibleIndex = findFirstVisibleIndex(layouts, 0, false);
expect(firstVisibleIndex).toBe(0);
});
// Test 5: No items are visible (threshold beyond all items)
it("returns -1 when no items are visible", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold is beyond all items (10 items * 100 height = 1000)
const firstVisibleIndex = findFirstVisibleIndex(layouts, 1100, false);
expect(firstVisibleIndex).toBe(-1);
});
// Test 6: Edge case - threshold exactly at item boundary
it("returns correct index when threshold is exactly at item boundary", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold exactly at the start of the 5th item
const firstVisibleIndex = findFirstVisibleIndex(layouts, 400, false);
expect(firstVisibleIndex).toBe(4);
});
// Test 7: Edge case - threshold in the middle of an item
it("returns correct index when threshold is in the middle of an item", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold in the middle of the 3rd item
const firstVisibleIndex = findFirstVisibleIndex(layouts, 250, false);
expect(firstVisibleIndex).toBe(2);
});
// Test 8: With grid layout - threshold crosses multiple columns
it("finds first visible index with grid layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
20,
{ maxColumns: 2 }
);
const layouts = getAllLayouts(layoutManager);
// With 2 columns, items are positioned differently
const firstVisibleIndex = findFirstVisibleIndex(layouts, 150, false);
// Expected result depends on how grid layout positions items
// This test might need adjustment based on actual grid layout behavior
expect(firstVisibleIndex).not.toBe(-1);
});
// Test 9: With masonry layout - variable height items
it("finds first visible index with masonry layout and variable item sizes", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.MASONRY,
20,
{ maxColumns: 2 },
100,
100,
true // Variable size
);
const layouts = getAllLayouts(layoutManager);
const firstVisibleIndex = findFirstVisibleIndex(layouts, 200, false);
expect(firstVisibleIndex).not.toBe(-1);
});
// Test 10: Partial visibility - item just starting to appear
it("finds item that is just starting to become visible", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold just 1px before item 4 ends
const firstVisibleIndex = findFirstVisibleIndex(layouts, 399, false);
expect(firstVisibleIndex).toBe(3);
});
});
describe("findLastVisibleIndex", () => {
// Test 11: Basic functionality - vertical layout
it("finds the last visible index in a vertical layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
20,
{ horizontal: false }
);
const layouts = getAllLayouts(layoutManager);
// Viewport ends at y=250, so the third item (index 2) should be last visible
const lastVisibleIndex = findLastVisibleIndex(layouts, 250, false);
expect(lastVisibleIndex).toBe(2);
});
// Test 12: Basic functionality - horizontal layout
it("finds the last visible index in a horizontal layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.LINEAR,
20,
{ horizontal: true }
);
const layouts = getAllLayouts(layoutManager);
// Viewport ends at x=250, so the third item (index 2) should be last visible
const lastVisibleIndex = findLastVisibleIndex(layouts, 250, true);
expect(lastVisibleIndex).toBe(2);
});
// Test 13: Empty layouts array
it("returns -1 for empty layouts array", () => {
const lastVisibleIndex = findLastVisibleIndex([], 100, false);
expect(lastVisibleIndex).toBe(-1);
});
// Test 14: All items are within viewport
it("returns the last item index when all items are within viewport", () => {
const layouts = createMockLayouts(5, 0, 100, false);
// Viewport ends at y=1000, which includes all 5 items
const lastVisibleIndex = findLastVisibleIndex(layouts, 1000, false);
expect(lastVisibleIndex).toBe(4); // Last item index is 4
});
// Test 15: No items are visible (threshold before all items)
it("returns -1 when no items are visible", () => {
const layouts = createMockLayouts(10, 100, 100, false);
// Threshold is before all items start
const lastVisibleIndex = findLastVisibleIndex(layouts, 50, false);
expect(lastVisibleIndex).toBe(-1);
});
// Test 16: Edge case - threshold exactly at item boundary
it("returns correct index when threshold is exactly at item boundary", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold exactly at the end of the 3rd item
const lastVisibleIndex = findLastVisibleIndex(layouts, 300, false);
expect(lastVisibleIndex).toBe(3);
});
// Test 17: Edge case - threshold in the middle of an item
it("returns correct index when threshold is in the middle of an item", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold in the middle of the 3rd item
const lastVisibleIndex = findLastVisibleIndex(layouts, 250, false);
expect(lastVisibleIndex).toBe(2);
});
// Test 18: With grid layout
it("finds last visible index with grid layout", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
20,
{ maxColumns: 2 }
);
const layouts = getAllLayouts(layoutManager);
const lastVisibleIndex = findLastVisibleIndex(layouts, 350, false);
expect(lastVisibleIndex).not.toBe(-1);
});
// Test 19: With masonry layout - variable height items
it("finds last visible index with masonry layout and variable item sizes", () => {
const layoutManager = createPopulatedLayoutManager(
LayoutManagerType.MASONRY,
20,
{ maxColumns: 2 },
100,
100,
true // Variable size
);
const layouts = getAllLayouts(layoutManager);
const lastVisibleIndex = findLastVisibleIndex(layouts, 400, false);
expect(lastVisibleIndex).not.toBe(-1);
});
// Test 20: Last item partially visible
it("includes last item when it's partially visible", () => {
const layouts = createMockLayouts(10, 0, 100, false);
// Threshold just 1px into the 5th item
const lastVisibleIndex = findLastVisibleIndex(layouts, 401, false);
expect(lastVisibleIndex).toBe(4);
});
});
describe("Edge cases and complex scenarios", () => {
// Test 21: Single item layout
it("correctly handles single item layout for first visible", () => {
const layouts = createMockLayouts(1, 0, 100, false);
const firstVisibleIndex = findFirstVisibleIndex(layouts, 50, false);
expect(firstVisibleIndex).toBe(0);
});
// Test 22: Single item layout
it("correctly handles single item layout for last visible", () => {
const layouts = createMockLayouts(1, 0, 100, false);
const lastVisibleIndex = findLastVisibleIndex(layouts, 50, false);
expect(lastVisibleIndex).toBe(0);
});
// Test 23: Variable size items for first visible index
it("correctly finds first visible with variable size items", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 50 },
{ x: 0, y: 50, width: 100, height: 150 },
{ x: 0, y: 200, width: 100, height: 75 },
{ x: 0, y: 275, width: 100, height: 100 },
];
const firstVisibleIndex = findFirstVisibleIndex(layouts, 175, false);
expect(firstVisibleIndex).toBe(1); // Second item is still visible at threshold 175
});
// Test 24: Variable size items for last visible index
it("correctly finds last visible with variable size items", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 50 },
{ x: 0, y: 50, width: 100, height: 150 },
{ x: 0, y: 200, width: 100, height: 75 },
{ x: 0, y: 275, width: 100, height: 100 },
];
const lastVisibleIndex = findLastVisibleIndex(layouts, 225, false);
expect(lastVisibleIndex).toBe(2); // Third item is visible at threshold 225
});
// Test 25: Items with zero size
it("correctly handles items with zero size for first visible", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 0 },
{ x: 0, y: 0, width: 100, height: 100 },
];
const firstVisibleIndex = findFirstVisibleIndex(layouts, 0, false);
expect(firstVisibleIndex).toBe(0); // First item is at position but has zero height
});
// Test 26: Items with zero size
it("correctly handles items with zero size for last visible", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 100 },
{ x: 0, y: 100, width: 100, height: 0 },
];
const lastVisibleIndex = findLastVisibleIndex(layouts, 100, false);
expect(lastVisibleIndex).toBe(1); // Second item is at threshold position but has zero height
});
// Test 27: Large number of items - performance test
it("efficiently finds first visible index in large dataset", () => {
const layouts = createMockLayouts(1000, 0, 100, false);
// Threshold in the middle of the list
const firstVisibleIndex = findFirstVisibleIndex(layouts, 50000, false);
expect(firstVisibleIndex).toBe(500);
});
// Test 28: Large number of items - performance test
it("efficiently finds last visible index in large dataset", () => {
const layouts = createMockLayouts(1000, 0, 100, false);
// Threshold in the middle of the list
const lastVisibleIndex = findLastVisibleIndex(layouts, 50000, false);
expect(lastVisibleIndex).toBe(500);
});
// Test 29: Non-sequential indices
it("works with non-sequential indices for first visible", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 100 },
{ x: 0, y: 100, width: 100, height: 100 },
{ x: 0, y: 200, width: 100, height: 100 },
];
const firstVisibleIndex = findFirstVisibleIndex(layouts, 150, false);
expect(firstVisibleIndex).toBe(1); // Second layout in the array, not index 1
});
// Test 30: Non-sequential indices
it("works with non-sequential indices for last visible", () => {
const layouts: RVLayout[] = [
{ x: 0, y: 0, width: 100, height: 100 },
{ x: 0, y: 100, width: 100, height: 100 },
{ x: 0, y: 200, width: 100, height: 100 },
];
const lastVisibleIndex = findLastVisibleIndex(layouts, 150, false);
expect(lastVisibleIndex).toBe(1); // Second layout in the array, not index 1
});
});
});