Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No rows returned by useVirtualizer in unit tests #641

Open
2 tasks done
densk1 opened this issue Jan 3, 2024 · 13 comments
Open
2 tasks done

No rows returned by useVirtualizer in unit tests #641

densk1 opened this issue Jan 3, 2024 · 13 comments

Comments

@densk1
Copy link

densk1 commented Jan 3, 2024

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.
@piecyk
Copy link
Collaborator

piecyk commented Jan 3, 2024

@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.

@densk1
Copy link
Author

densk1 commented Jan 3, 2024

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?

@ak274
Copy link

ak274 commented Jan 5, 2024

@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?

@luchiago
Copy link

luchiago commented Jan 5, 2024

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,
        }
    });

@zroux93
Copy link

zroux93 commented Jan 5, 2024

@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?

@luchiago
Copy link

luchiago commented Jan 5, 2024

@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.

@saomartinho
Copy link

saomartinho commented Jan 16, 2024

@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?

@luchiago
Copy link

@saomartinho can you share the code problem here?

@istvandesign
Copy link

istvandesign commented Jan 20, 2024

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.

@luchiago
Copy link

@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.

@istvandesign
Copy link

@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.

@piecyk
Copy link
Collaborator

piecyk commented Jan 20, 2024

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,
})

@ipsips
Copy link

ipsips commented Feb 19, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants