Skip to content
Open
92 changes: 92 additions & 0 deletions src/__tests__/GridLayoutManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,98 @@ describe("GridLayoutManager", () => {
});
});

describe("Separator behavior", () => {
it("should mark last row items to skip separators in 2x2 grid", () => {
const manager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
4,
defaultParams
);
const layouts = getAllLayouts(manager);

// First row items should not skip separators
expect(layouts[0].skipSeparator).toBeFalsy();
expect(layouts[1].skipSeparator).toBeFalsy();

// Last row items should skip separators
expect(layouts[2].skipSeparator).toBe(true);
expect(layouts[3].skipSeparator).toBe(true);
});

it("should mark last row items to skip separators in 3x3 grid", () => {
const manager = createPopulatedLayoutManager(LayoutManagerType.GRID, 6, {
...defaultParams,
maxColumns: 3,
});
const layouts = getAllLayouts(manager);

// First row items should not skip separators
expect(layouts[0].skipSeparator).toBeFalsy();
expect(layouts[1].skipSeparator).toBeFalsy();
expect(layouts[2].skipSeparator).toBeFalsy();

// Last row items should skip separators
expect(layouts[3].skipSeparator).toBe(true);
expect(layouts[4].skipSeparator).toBe(true);
expect(layouts[5].skipSeparator).toBe(true);
});

it("should mark last row items to skip separators with uneven rows", () => {
const manager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
5,
defaultParams
);
const layouts = getAllLayouts(manager);

// First two rows should not skip separators
expect(layouts[0].skipSeparator).toBeFalsy();
expect(layouts[1].skipSeparator).toBeFalsy();
expect(layouts[2].skipSeparator).toBeFalsy();
expect(layouts[3].skipSeparator).toBeFalsy();

// Last row (single item) should skip separator
expect(layouts[4].skipSeparator).toBe(true);
});

it("should handle single item grid", () => {
const manager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
1,
defaultParams
);
const layouts = getAllLayouts(manager);

// Single item should skip separator
expect(layouts[0].skipSeparator).toBe(true);
});

it("should update skipSeparator when items are added dynamically", () => {
const manager = createPopulatedLayoutManager(
LayoutManagerType.GRID,
2,
defaultParams
);
let layouts = getAllLayouts(manager);

// Initially, both items are in last row
expect(layouts[0].skipSeparator).toBe(true);
expect(layouts[1].skipSeparator).toBe(true);

// Add two more items to complete the grid
manager.modifyLayout([], 4);
layouts = getAllLayouts(manager);

// First row should not skip separators anymore
expect(layouts[0].skipSeparator).toBeFalsy();
expect(layouts[1].skipSeparator).toBeFalsy();

// New last row should skip separators
expect(layouts[2].skipSeparator).toBe(true);
expect(layouts[3].skipSeparator).toBe(true);
});
});

describe("Layout recalculations", () => {
it("should adjust layout when window size changes", () => {
const manager = createPopulatedLayoutManager(
Expand Down
253 changes: 253 additions & 0 deletions src/__tests__/ViewHolder.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import React from "react";
import { Text, View } from "react-native";
import "@quilted/react-testing/matchers";
import { render } from "@quilted/react-testing";

import { ViewHolder } from "../recyclerview/ViewHolder";
import type { RVLayout } from "../recyclerview/layout-managers/LayoutManager";
import type { ViewHolderProps } from "../recyclerview/ViewHolder";

// Mock CompatView component
jest.mock("../recyclerview/components/CompatView", () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactLib = require("react");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { View: ViewComponent } = require("react-native");

const CompatView = ReactLib.forwardRef((props: any, ref: any) => {
const { children, ...otherProps } = props;
return ReactLib.createElement(
ViewComponent,
{ ref, ...otherProps },
children
);
});

CompatView.displayName = "CompatView";

return { CompatView };
});

describe("ViewHolder", () => {
const mockRefHolder = new Map();
const mockRenderItem = jest.fn(({ item }) => <Text>{item.text}</Text>);
const mockSeparatorComponent = jest.fn(({ leadingItem, trailingItem }) => (
<View>
<Text>
Separator between {leadingItem.text} and {trailingItem.text}
</Text>
</View>
));

const defaultLayout: RVLayout = {
x: 0,
y: 0,
width: 100,
height: 50,
isWidthMeasured: true,
isHeightMeasured: true,
};

const defaultProps: ViewHolderProps<{ text: string }> = {
index: 0,
layout: defaultLayout,
refHolder: mockRefHolder,
extraData: null,
target: "Cell",
item: { text: "Item 1" },
trailingItem: { text: "Item 2" },
renderItem: mockRenderItem,
ItemSeparatorComponent: mockSeparatorComponent,
horizontal: false,
};

beforeEach(() => {
jest.clearAllMocks();
mockRefHolder.clear();
});

describe("Separator rendering", () => {
it("should render separator when skipSeparator is false", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: false }}
/>
);

expect(result).toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
expect(mockSeparatorComponent).toHaveBeenCalledWith(
{
leadingItem: { text: "Item 1" },
trailingItem: { text: "Item 2" },
},
{}
);
});

it("should render separator when skipSeparator is undefined", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: undefined }}
/>
);

expect(result).toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
expect(mockSeparatorComponent).toHaveBeenCalledWith(
{
leadingItem: { text: "Item 1" },
trailingItem: { text: "Item 2" },
},
{}
);
});

it("should not render separator when skipSeparator is true", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: true }}
/>
);

expect(result).not.toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
expect(mockSeparatorComponent).not.toHaveBeenCalled();
});

it("should not render separator when trailingItem is undefined", () => {
const result = render(
<ViewHolder
{...defaultProps}
trailingItem={undefined}
layout={{ ...defaultLayout, skipSeparator: false }}
/>
);

expect(result).not.toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
expect(mockSeparatorComponent).not.toHaveBeenCalled();
});

it("should not render separator when ItemSeparatorComponent is undefined", () => {
const result = render(
<ViewHolder
{...defaultProps}
ItemSeparatorComponent={undefined}
layout={{ ...defaultLayout, skipSeparator: false }}
/>
);

expect(result).not.toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
expect(mockSeparatorComponent).not.toHaveBeenCalled();
});
});

describe("Memoization behavior", () => {
it("should re-render when skipSeparator changes from false to true", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: false }}
/>
);

// Initially separator should be rendered
expect(result).toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});

// Change skipSeparator to true
result.setProps({
layout: { ...defaultLayout, skipSeparator: true },
});

// Separator should no longer be rendered
expect(result).not.toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
});

it("should re-render when skipSeparator changes from true to false", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: true }}
/>
);

// Initially separator should not be rendered
expect(result).not.toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});

// Change skipSeparator to false
result.setProps({
layout: { ...defaultLayout, skipSeparator: false },
});

// Separator should now be rendered
expect(result).toContainReactComponent(Text, {
children: ["Separator between ", "Item 1", " and ", "Item 2"],
});
});
});

describe("Item rendering", () => {
it("should always render the item content regardless of skipSeparator", () => {
const result = render(
<ViewHolder
{...defaultProps}
layout={{ ...defaultLayout, skipSeparator: true }}
/>
);

expect(result).toContainReactComponent(Text, { children: "Item 1" });
expect(mockRenderItem).toHaveBeenCalledWith({
item: { text: "Item 1" },
index: 0,
extraData: null,
target: "Cell",
});

// Re-render with skipSeparator false
result.setProps({
layout: { ...defaultLayout, skipSeparator: false },
});

expect(result).toContainReactComponent(Text, { children: "Item 1" });
});
});

describe("Layout styles", () => {
it("should apply layout styles correctly regardless of skipSeparator", () => {
const customLayout: RVLayout = {
x: 10,
y: 20,
width: 200,
height: 100,
skipSeparator: true,
enforcedWidth: true,
enforcedHeight: true,
};

const result = render(
<ViewHolder {...defaultProps} layout={customLayout} />
);

// Verify the item is rendered with correct content
expect(result).toContainReactComponent(Text, { children: "Item 1" });
// Note: Layout style verification would require more complex testing setup
// as @quilted/react-testing doesn't provide direct style inspection
});
});
});
Loading