Skip to content

No rows returned by useVirtualizer in unit tests #641

Open

Description

@densk1

Describe the bug

In browser table rows are rendered as expected. In unit tests useVirtualizer returns an empty array from rowVirtualizer.getVirtualItems(). The bug is introduce with changes introduced by v3.0.0-beta.63. It also exists in the latest release v3.0.1

I think it was introduced by #598 here

Your minimal, reproducible example

below

Steps to reproduce

This unit test will pass for v3.0.0-beta.62 and fail for any version there after.

import { useVirtualizer } from "@tanstack/react-virtual";
import { render, screen } from "@testing-library/react";
import { styled } from "@mui/material";
import { useCallback, useRef } from "react";

const items = [
  { id: 1, name: "foo" },
  { id: 2, name: "bar" },
];

const Parent = styled("div")({ height: "100%", width: "100%", overflow: "auto" });

const Inner = styled("div")({ width: "100%", position: "relative" });

const Row = styled("div")({ position: "absolute", top: 0, left: 0, width: "100%" });

function VirtualTableComponent() {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: useCallback(() => 56, []),
  });

  return (
    <Parent ref={parentRef}>
      <Inner sx={{ height: `${rowVirtualizer.getTotalSize()}px` }}> {/* set at 112px as expected */}
        {rowVirtualizer.getVirtualItems().map((virtualRow) => ( // always an empty array
          <Row
            key={virtualRow.index}
            data-index={virtualRow.index}
            ref={rowVirtualizer.measureElement}
            style={{ transform: `translateY(${virtualRow.start}px)` }}
          >
            <div>{items[virtualRow.index].name}</div>
          </Row>
        ))}
      </Inner>
    </Parent>
  );
}

describe("@tanstack/react-virtual", () => {
  it("renders rows", () => {
    render(<VirtualTableComponent />);

    expect(screen.getByText(items[0].name)).toBeInTheDocument();
    expect(screen.getByText(items[1].name)).toBeInTheDocument();
  });
});

Expected behavior

As a user I expect the provided unit test to pass but it fails.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

macOS

tanstack-virtual version

v3.0.1

TypeScript version

v4.9.5

Additional context

I've console.log'd out the value of rowVirtualizer.getTotalSize() and it is given 112 as expected

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Activity

  1. piecyk commented on Jan 3, 2024

    @piecyk
    Collaborator

    @densk1 that is correct behaviour, it's because in unit test elements don't have height or width. One option is to mock the getBoundingClientRect for the scroller element to return some height.

  2. densk1 commented on Jan 3, 2024

    @densk1
    Author

    Ah ok, thanks! So it won't matter if set height on parent CSS because getBoundingClientRect won't return it anyway?

    Is there a challenge with mocking getBoundingClientRect that it needs to be mocked for both the parent, inner and rows themselves?

  3. ak274 commented on Jan 5, 2024

    @ak274

    @densk1 that is correct behaviour, it's because in unit test elements don't have height or width. One option is to mock the getBoundingClientRect for the scroller element to return some height.

    I'm facing the same issue rowVirtualizer.getVirtualItems() is always empty. Is there any example how to write unit tests for this?

  4. luchiago commented on Jan 5, 2024

    @luchiago

    Same issue here using 3.0.1. The proposed solution One option is to mock the getBoundingClientRect for the scroller element to return some height. worked on my test suite.

    Element.prototype.getBoundingClientRect = jest.fn(() => {
            return {
                width: 120,
                height: 120,
                top: 0,
                left: 0,
                bottom: 0,
                right: 0,
            }
        });
  5. zroux93 commented on Jan 5, 2024

    @zroux93

    @luchiago When I tried this, it changed the sizes of all HTML elements (both the container and the option elements), so my test list only rendered 2 options (presumably a visible one and the option below it) even though I passed more options to it. Is this how your tests are written or do you have a workaround for this?

  6. luchiago commented on Jan 5, 2024

    @luchiago

    @zroux93 In my case, the whole test file was failing for the same reason, the same as you. The tests passed when I put a beforeEach with this workaround. But before it started failing, I only expected an array with two virtual rows. That may be why it worked for me.

  7. saomartinho commented on Jan 16, 2024

    @saomartinho

    @luchiago, can you put a example? Because a follow your suggestion, and put beforeEach, but still return 0 virtual rows.

    Oki, found it my issue is in the logic file, where I use the renderHook function from testing-library. Do you know what to do here?

  8. luchiago commented on Jan 17, 2024

    @luchiago

    @saomartinho can you share the code problem here?

  9. istvandesign commented on Jan 20, 2024

    @istvandesign

    I think the issue here is that we should have unit tests in the examples.

    image

    Documentation should contain at least how to mock getBoundingClientRect in jest with JS and TypeScript to test with TanStack/virtual.
    I've wasted a day trying to figure out why my mock data was not working in a unit test, if you are refactoring someone else's code and you update react-virtual you will encounter this issue.

    Do we have some other way to do this, like mocking some function inside the library ?

    I've checked

      jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
        width: 120,
        height: 120,
        top: 0,
        left: 0,
        bottom: 0,
        right: 0,
        x: 0,
        y: 0,
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        toJSON: () => {}
      }));

    and

    Object.defineProperty(Element.prototype, 'getBoundingClientRect', {  
      configurable: true,  
      value: jest.fn(() => {  
        return {  
          width: 120,  
          height: 120,  
          top: 0,  
          left: 0,  
          bottom: 0,  
          right: 0,  
          x: 0,  
          y: 0,  
          toJSON: () => {},  
        };  
      }),  
    });  

    both solutions allow the tests to render the items in a virtualised table.

  10. luchiago commented on Jan 20, 2024

    @luchiago

    @istvandesign it's a similar approach to what I did, but you did it with more options. I agree it's missing a documentation on how to mock, but I believe it should be an issue to investigate.

  11. istvandesign commented on Jan 20, 2024

    @istvandesign

    @istvandesign it's a similar approach to what I did, but you did it with more options. I agree it's missing a documentation on how to mock, but I believe it should be an issue to investigate.

    That's because I am using TypeScript.

  12. piecyk commented on Jan 20, 2024

    @piecyk
    Collaborator

    Documentation should contain at least how to mock getBoundingClientRect in jest with JS and TypeScript to test with TanStack/virtual.

    Yes i agree, we should include it. Nevertheless mocking Element.prototype is enough, for more complex scenarios we can even return different values, for example like here if we add aria-label on scroller div of virtualizer

    const getDOMRect = (width: number, height: number) => ({
      width,
      height,
      top: 0,
      left: 0,
      bottom: 0,
      right: 0,
      x: 0,
      y: 0,
      toJSON: () => {},
    })
    
    beforeEach(() => {
      Element.prototype.getBoundingClientRect = jest.fn(function () {
        if (this.getAttribute('aria-label') === 'Virtual list') {
          return getDOMRect(500, 500)
        }
        return getDOMRect(0, 0)
      })
    })

    Do we have some other way to do this, like mocking some function inside the library ?

    Other option would be to create context provider that would pass your own observeElementRect for test, something like

    const VirtualizerObserveElementRect = React.createContext<
      ((instance: Virtualizer<any, any>, cb: (rect: { width: number; height: number }) => void) => void) | undefined
    >(undefined)
    
    // in test wrap with provider 
    
    const Mock = ({ children }: { children: React.ReactNode }) => {
      return (
        <VirtualizerObserveElementRect.Provider
          value={(_, cb) => {
            cb({ height: 200, width: 200 })
          }}
        >
          {children}
        </VirtualizerObserveElementRect.Provider>
      )
    }
    
    // and in component 
    
    const observeElementRect = React.useContext(VirtualizerObserveElementRect)
    const virtualizer = useVirtualizer({
      count: 1000,
      observeElementRect,
      estimateSize: () => 25,
      getScrollElement: () => null,
    })
  13. ipsips commented on Feb 19, 2024

    @ipsips

    i am facing the same problem but not in simulated test environment:

    it seems to me that if getScrollElement returns null, then getVirtualItems returns an empty array.
    and it seems that getScrollElement is called during initial render and if you've chosen to return a ref object's current value (which is expected to point to DOM element) then you are not doing React correctly because:

    • "During the first render, the DOM nodes have not yet been created, so ref.current will be null." from the docs
    • "Do not write or read ref.current during rendering." also from the docs

    coming up with a workaround to this should be trivial, however using a ref object to refer to a DOM node does seem like to most natural approach in getScrollElement.

  14. denchen commented on May 5, 2025

    @denchen

    Sorry to dredge up an old topic, but I had been using the mocking of getBoundingClientRect() from #641 (comment) successfully, but this no longer works with @TanStack/virtual@3.13.7 (published on May 5th). I believe it's due to this change -- #986 -- with no longer using getBoundingClientRect().

    Width this change, I'm unsure with how to properly mock the calculated width / height.

    EDIT: The following is a new workaround that passes my tests:

      beforeEach(() => {
        Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
          value: 800
        });
        Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
          value: 800
        });
      });
  15. shniubobo commented on May 12, 2025

    @shniubobo

    Sorry to dredge up an old topic, but I had been using the mocking of getBoundingClientRect() from #641 (comment) successfully, but this no longer works with @TanStack/virtual@3.13.7 (published on May 5th). I believe it's due to this change -- #986 -- with no longer using getBoundingClientRect().

    Width this change, I'm unsure with how to properly mock the calculated width / height.

    EDIT: The following is a new workaround that passes my tests:

    beforeEach(() => {
    Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
    value: 800
    });
    Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
    value: 800
    });
    });

    Thank you very much for the new workaround! In my case, it has worked even without beforeEach.

  16. piecyk commented on May 12, 2025

    @piecyk
    Collaborator

    Yes, that's correct! We’ve transitioned to using offsetHeight and offsetWidth for measuring the scroller's size. To mock these values properly, you’ll need to adjust your mocks accordingly. This definitely should be mentioned in the documentation to avoid confusion. I'll make sure to bring it up.

  17. nataliethistime commented on Oct 7, 2025

    @nataliethistime

    For anyone interested, another way of solving this is to pass in a custom element rect observer that returns the value you need. We had to take this approach as we were mocking ResizeObserver to do nothing due to other parts of our application.

    The usage of this function is documented here and you can also refer to the original implementation if it helps.

    jest.mock<typeof import('@tanstack/react-virtual')>('@tanstack/react-virtual', () => {
      const actual = jest.requireActual<typeof import('@tanstack/react-virtual')>('@tanstack/react-virtual')
      return {
        ...actual,
        useVirtualizer(options) {
          return actual.useVirtualizer({
            ...options,
            observeElementRect: (instance, callback) => {
              if (instance.scrollElement) callback({ width: 10_000, height: 10_000 })
            },
          })
        },
      }
    })
  18. hashcode-adrien commented on Oct 28, 2025

    @hashcode-adrien

    I wrote a custom VirtualListBox for joyui Autocomplete which uses @tanstack/react-virtual instead of react-window.

    I share the code to make it work as expected with vitest :

    import { Autocomplete } from '@mui/joy';
    import { render, screen } from '@testing-library/react';
    import { expect, test } from 'vitest';
    import VirtualListBox from '../joyui/VirtualListBox';
    
    // Save original measurement APIs so we can restore them after the test.
    const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
    
    test('renders Autocomplete with VirtualListBox and check if virtualbox_option2 is visible', async () => {
      try {
        Object.defineProperties(HTMLElement.prototype, {
          clientHeight: {
            configurable: true,
            get() {
              return 500;
            }
          },
          clientWidth: {
            configurable: true,
            get() {
              return 500;
            }
          },
          offsetHeight: {
            configurable: true,
            get() {
              return 500;
            }
          },
          offsetWidth: {
            configurable: true,
            get() {
              return 500;
            }
          }
        });
        render(
          <Autocomplete
            sx={{ height: 500 }}
            options={["virtualbox_option1", "virtualbox_option2"]}
            slots={{ listbox: VirtualListBox }}
            open={true}
            renderOption={(props, option: string) => [props, option] as React.ReactNode}
          />
        );
    
        // wait for an option to appear; findByText will retry until it finds the element
        const opt = await screen.findByText(/virtualbox_option2/);
        expect(opt).toBeTruthy();
      } finally {
        // restore the original to avoid side effects for other tests
        Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
      }
    });
  19. ToshiakiOmori commented on Oct 28, 2026

    @ToshiakiOmori

    A heads-up for anyone mocking element sizes in unit tests (happy-dom / jsdom), following the offsetHeight / offsetWidth advice earlier in this thread:

    Since @tanstack/virtual-core 3.19.6 (#1326, "Measure the scroll viewport using client dimensions"), the scroll element's size is read from clientHeight / clientWidth, while items are still measured with offsetHeight / offsetWidth. If you only mock the offset* getters, the viewport size is 0 and getVirtualItems() returns nothing again.

    In our case this showed up as Headless UI's virtual Combobox rendering no options in tests after a patch update (@tanstack/react-virtual 3.14.13 → 3.14.23), while it kept working in a real browser.

    Mocking both pairs fixed it for us. Note that in happy-dom these getters live on HTMLElement.prototype, not Element.prototype. We return sizes only for the listbox and its options so that other code reading client* (e.g. overflow checks) keeps seeing 0:

    const sizeOf = (el: HTMLElement, listbox: number, option: number) => {
      const role = el.getAttribute('role');
      return role === 'listbox' ? listbox : role === 'option' ? option : 0;
    };
    
    vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (this: HTMLElement) { return sizeOf(this, 256, 40); });
    vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => 320);
    vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(function (this: HTMLElement) { return sizeOf(this, 256, 40); });
    vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function (this: HTMLElement) { return sizeOf(this, 320, 320); });

    Question for the maintainers: is mocking the getters like this the recommended approach going forward, or would you suggest passing a custom observeElementRect / initialRect in tests instead? A short note in the testing docs about which properties are read for the scroll element vs. the items would help, since this has changed twice (getBoundingClientRect → offset* in 3.13.7, offset* → client* in 3.19.6).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions