/** * @jest-environment jsdom */ import React from "react"; import { render } from "@testing-library/react"; import { VirtualTable } from "./VirtualTable"; import { VirtualTableProps } from "./VirtualTableProps"; // Counts executions of the row component body. The real VirtualTableRow is // swapped for a memoized stub so the count reflects React's bail-out decision // and nothing else. const renderCallback = jest.fn(); jest.mock("./VirtualTableRow", () => { const React = require("react"); // Memoized like the real component: without this the stub would re-render // on any parent render and the count would say nothing about the context. const VirtualTableRow = React.memo((props: any) => { renderCallback(); return
{props.children}
; }); VirtualTableRow.displayName = "VirtualTableRow"; return { VirtualTableRow }; }); // Fixed bounds: the real hook measures nothing in jsdom, so the list would // render zero rows. jest.mock("react-use-measure", () => { return () => [ (element: any) => {}, { width: 500, height: 500, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0 } ]; }); // jsdom has no ResizeObserver, and VirtualTable passes the global in as the // `react-use-measure` polyfill — the identifier has to resolve even though the // mock above means it is never constructed. global.ResizeObserver = class ResizeObserver { observe() {} unobserve() {} disconnect() {} }; describe("VirtualTable Performance", () => { beforeEach(() => { renderCallback.mockClear(); }); it("does not re-render rows when VirtualTable re-renders but data is unchanged", async () => { const columns = [{ key: "col1", title: "Column 1", width: 100 }]; const data = Array.from({ length: 10 }).map((_, i) => ({ col1: `Value ${i}` })); const cellRenderer = () =>
Cell
; const props: VirtualTableProps = { data, columns, rowHeight: 50, cellRenderer }; const { rerender } = render(); const initialRenderCount = renderCallback.mock.calls.length; expect(initialRenderCount).toBe(data.length); // `className` is deliberately NOT one of the context memo's dependencies: // changing it forces VirtualTable's body to run again (React.memo on the // table itself would otherwise bail out) while every value the rows read // stays identical. rerender(); // Rows read everything through VirtualListContext, and the value handed // to the provider is memoized. If that memo goes away the provider gets a // fresh object, every Consumer re-runs, and each one hands the row a new // `style` object and a new children array — which defeats React.memo and // re-renders all 10 rows. So an unchanged count is the whole assertion. expect(renderCallback.mock.calls.length).toBe(initialRenderCount); }); });