UI Component Testing Guide

How to Test React-Window List with Playwright: Virtualized Scrolling, Variable Heights, and Off-DOM Items

A scenario-by-scenario walkthrough of testing virtualized lists built with react-window and react-virtuoso using Playwright. Off-DOM item queries, scroll-to-index verification, variable-height rows, overscan count tuning, and the pitfalls that silently break virtualized list test suites.

4.2M+

react-window receives over 4.2 million weekly npm downloads, making it one of the most widely used virtualization libraries in the React ecosystem.

npm registry, 2026

0msScroll settle wait
0Scenarios covered
0%Items off-DOM at any time
0%Fewer lines with Assrt

Virtualized List Render Cycle

Test ScriptBrowser DOMreact-windowScroll ContainerVisible RowsQuery list item at index 500Not found (off-DOM)Scroll container to offsetonScroll event firesRecalculate visible rangeMount items 495-510Query item at index 500Element found, assert content

1. Why Testing Virtualized Lists Is Harder Than It Looks

Virtualized lists render only the items currently visible in the viewport plus a small overscan buffer. A list of 10,000 rows might have only 15 to 20 DOM nodes at any given moment. The rest simply do not exist in the document. This is the core performance optimization that makes react-window and react-virtuoso viable for large datasets, but it creates a fundamental problem for testing: you cannot query an element that is not in the DOM.

Traditional Playwright locators like page.getByText("Row 500") will time out because row 500 has not been rendered yet. The item exists in your data array but not in the browser document. To reach it, your test must first scroll the container to the correct offset, wait for react-window to recalculate the visible range and mount the new items, and only then query for the element. This scroll, wait, query cycle is the fundamental pattern for every virtualized list test.

Five structural factors make this harder than standard list testing. First, the absolute positioning model: react-window positions each item with position: absolute and a calculated top offset, so standard DOM order traversal does not reflect visual order. Second, variable-height rows require a measurement function that runs after mount, creating a double-render cycle where items initially appear at the wrong position and then snap into place. Third, the overscan count (the number of extra items rendered above and below the visible area) varies by library and configuration, so the exact set of mounted items is unpredictable. Fourth, scroll events are asynchronous, and react-window debounces them internally, meaning your test must wait for the scroll to settle before asserting. Fifth, libraries like react-virtuoso use a completely different internal architecture (CSS transforms vs absolute positioning), so tests written for one library may fail silently when migrated to another.

Why Standard Locators Fail on Virtualized Lists

⚙️

10,000 Items

In data array

🌐

Virtualization

react-window calculates visible range

~15 DOM Nodes

Only visible items mounted

Query Row 500

Not in DOM, locator times out

↪️

Scroll First

Move viewport to offset

🌐

Re-render

New items mount

Query Succeeds

Row 500 now in DOM

2. Setting Up Your Test Environment

Before writing any virtualized list tests, you need a Playwright project configured with consistent viewport dimensions and a helper utility for scrolling within containers. Viewport size directly controls how many items react-window renders, so inconsistent dimensions between local development and CI will cause different items to be visible, leading to flaky assertions.

Install dependencies
playwright.config.ts

Scroll Helper Utility

The single most important helper for virtualized list testing is a function that scrolls a container to a pixel offset and waits for the scroll to settle. React-window fires scroll events asynchronously and debounces re-renders, so you need a small stability check after scrolling. The helper below scrolls the container and then polls until the scrollTop value stabilizes.

tests/helpers/scroll-utils.ts

Test Environment Setup Flow

🌐

Install Playwright

npm init playwright@latest

🔒

Fix Viewport

1280x720 in config

⚙️

Scroll Helpers

scrollToOffset, scrollToIndex

Dev Server

webServer in config

1

Verifying Initial Render and Visible Window

Straightforward

3. Scenario: Verifying Initial Render and Visible Window

Goal: Confirm that the list renders the correct number of visible items on initial load, that the first item is visible, and that items beyond the visible window plus overscan are not present in the DOM.

Preconditions: A react-window FixedSizeList with 1,000 items, each 35px tall, in a container 400px high. The default overscan count is 1.

The visible window fits approximately 11 items (400 / 35, rounded up), and with an overscan of 1, react-window renders about 13 items total. Your test should verify that the first item is visible, that approximately 13 items exist in the DOM, and that item 50 is not present.

tests/virtualized-list/initial-render.spec.ts

What to assert beyond the UI:Verify the inner container's total height matches the expected value (item count multiplied by item height). This confirms react-window has correctly calculated the scrollable area. If this value is wrong, scroll-to-index calculations will land on the wrong item.

2

Scroll-to-Index Navigation

Moderate

4. Scenario: Scroll-to-Index Navigation

Goal: Use react-window's scrollToItem API (triggered by a UI button or exposed ref) to jump to a specific row and verify it appears in the visible area.

Preconditions:The list exposes a "Jump to row" input that accepts an index number and calls listRef.current.scrollToItem(index) on submit.

The key challenge here is timing. After scrollToItemis called, react-window updates the container's scrollTop synchronously, but the visible items re-render asynchronously. Your test must wait for the target item to appear in the DOM before asserting on its content. A naive approach of querying immediately after the scroll call will fail because the new items have not mounted yet.

tests/virtualized-list/scroll-to-index.spec.ts

Scroll-to-Index: Playwright vs Assrt

test('scrollToItem navigates to row', async ({ page }) => {
  await page.goto('/list-demo');
  const jumpInput = page.locator('[data-testid="jump-to-input"]');
  const jumpButton = page.locator('[data-testid="jump-to-button"]');
  await jumpInput.fill('500');
  await jumpButton.click();
  const targetItem = page.locator('[data-testid="list-item-500"]');
  await expect(targetItem).toBeVisible({ timeout: 5000 });
  await expect(targetItem).toHaveText(/Item 500/);
  const neighbor = page.locator('[data-testid="list-item-501"]');
  await expect(neighbor).toBeAttached();
});
33% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
3

Variable-Height Rows

Complex

5. Scenario: Variable-Height Rows

Goal: Test a VariableSizeList where each row has a different height based on its content length. Verify that scroll-to-index still lands on the correct item and that the total scroll height reflects the sum of all individual row heights.

Why this is hard: With FixedSizeList, the scroll offset for any index is simply index * itemHeight. With VariableSizeList, react-window must sum the heights of all preceding items to calculate the offset. If your itemSizefunction returns an estimated height and the actual rendered height differs, items will visually jump as react-window corrects the positions. This "content shift" breaks tests that check bounding boxes immediately after scrolling.

React-virtuoso handles this differently by measuring items after render and adjusting positions dynamically. This means react-virtuoso tests must account for a measurement phase where items may temporarily have incorrect positions. The test below handles both cases.

tests/virtualized-list/variable-height.spec.ts

What to assert beyond the UI: When testing variable-height lists, always verify the total scroll height of the inner container. If the itemSize function returns incorrect values, the scroll height will be wrong and every scroll-to-index operation will land on the wrong item. Catching the scroll height mismatch early prevents an entire class of downstream assertion failures.

4

Keyboard Navigation and Focus Management

Moderate

6. Scenario: Keyboard Navigation and Focus Management

Goal: Verify that arrow key navigation moves focus through list items, that the list scrolls to keep the focused item visible, and that focus is not lost when items are recycled (unmounted and remounted by the virtualizer).

Why this is tricky: When the user presses the down arrow and the focused item scrolls out of the visible window, react-window unmounts that DOM element. If your application stores focus on the DOM node itself (via tabIndex and ref.focus()), the focus disappears when the node is recycled. Accessible virtualized lists must manage focus at the container level using aria-activedescendant instead of moving DOM focus to each item. Your test must verify both the visual scroll position and the aria state.

tests/virtualized-list/keyboard-nav.spec.ts
5

Dynamic Data Loading (Infinite Scroll)

Complex

7. Scenario: Dynamic Data Loading (Infinite Scroll)

Goal: Test that scrolling to the bottom of the list triggers a data fetch, that a loading indicator appears during the fetch, that new items are appended without disturbing the scroll position, and that the list stops fetching when all pages are loaded.

Preconditions: The list loads 50 items initially and fetches 50 more when the user scrolls within 200px of the bottom. The API returns paginated data with a hasMore flag.

Infinite scroll with virtualization is one of the hardest patterns to test because it combines two asynchronous behaviors: the scroll-triggered re-render from react-window and the network request for new data. Your test must coordinate both. The approach below uses Playwright route interception to control the API response timing, ensuring deterministic test behavior.

tests/virtualized-list/infinite-scroll.spec.ts

Infinite Scroll: Playwright vs Assrt

test('infinite scroll loads next page', async ({ page }) => {
  let requestCount = 0;
  await page.route('**/api/list*', async (route) => {
    requestCount++;
    const url = new URL(route.request().url());
    const pageNum = parseInt(url.searchParams.get('page') || '0');
    const items = generateItems(pageNum, 50);
    await route.fulfill({
      status: 200, contentType: 'application/json',
      body: JSON.stringify({ items, hasMore: pageNum < 3 }),
    });
  });
  await page.goto('/list-demo?mode=infinite');
  const container = page.locator('[data-testid="virtual-list"]');
  await container.evaluate((el) => {
    el.scrollTop = el.scrollHeight - el.clientHeight - 100;
  });
  await expect(page.locator('[data-testid="loading-indicator"]')).toBeVisible();
  await expect(page.locator('[data-testid="loading-indicator"]')).not.toBeVisible();
  expect(requestCount).toBe(2);
});
58% fewer lines
6

Overscan Count and Scroll Performance

Moderate

8. Scenario: Overscan Count and Scroll Performance

Goal: Verify that the overscan count configuration affects how many extra items are rendered beyond the visible window, and measure scroll performance to ensure the overscan setting provides smooth scrolling without excessive DOM node creation.

The overscanCount prop in react-window controls how many items are rendered above and below the visible area. A higher overscan count means smoother scrolling (fewer blank flashes) but more DOM nodes. A lower count means better initial render performance but a higher risk of the user seeing blank space during fast scrolling. Testing this involves counting rendered nodes and measuring frame timing during scroll operations.

tests/virtualized-list/overscan.spec.ts
Running overscan tests

9. Common Pitfalls That Break Virtualized List Tests

After building dozens of virtualized list test suites, these are the failure modes that show up repeatedly. Each one has caused real CI failures reported in react-window and react-virtuoso GitHub issues.

Pitfalls to Watch For

  • Querying items without scrolling first. The most common mistake. Items beyond the visible window do not exist in the DOM. Your test must scroll before querying. react-window GitHub issue #470 documents this confusion explicitly.
  • Hardcoding rendered item counts. The exact number of rendered items depends on container height, item height, and overscan count. If your CI environment has a different viewport height than local development, the count changes. Use range assertions (greaterThan / lessThan) instead of exact equality.
  • Asserting on bounding boxes immediately after scroll. react-window debounces scroll events and re-renders asynchronously. If you measure an item's position in the same tick as the scroll, you will get stale coordinates. Always include a settle wait.
  • Mixing react-window and react-virtuoso selectors. react-window uses position: absolute with top offsets on each item. react-virtuoso uses CSS transforms (translateY) on a wrapper div. Tests written for one library's DOM structure will fail silently on the other. Check which library you are testing and adjust selectors accordingly.
  • Forgetting the inner container height check. If the inner container's height is wrong (often caused by a broken itemSize function), every scroll-to-index operation will land on the wrong item. Assert the total height as a precondition in your test setup.
  • Using page.mouse.wheel for scrolling. Mouse wheel events are intercepted and transformed by react-window's own scroll handler, which can apply smoothing or momentum. Setting scrollTop directly via evaluate is more deterministic for tests.
  • Not accounting for the double-render in VariableSizeList. When items have dynamic heights measured after mount, react-window renders once with estimated heights and then re-renders with measured heights. Your test may catch items at intermediate positions if you assert too quickly.
  • Ignoring recycled component state. When items scroll out of view and new items reuse the same DOM node, any local state (hover, selection highlight, input values) from the previous item can leak into the new one. Test that recycled items display fresh state.

react-window vs react-virtuoso: DOM Structure Comparison

Understanding the DOM structure difference between these two libraries is critical for writing portable tests. Here is what the rendered output looks like for each:

DOM structure comparison

Pre-flight Checklist for Virtualized List Tests

  • Fixed viewport size in playwright.config.ts (prevents item count drift between environments)
  • Scroll helper utility with settle wait (prevents asserting on stale positions)
  • data-testid attributes on list items include the index (enables targeting specific rows)
  • Inner container height assertion in test setup (catches broken itemSize functions early)
  • Range-based item count assertions, not exact counts (handles overscan and viewport variations)
  • Route interception for infinite scroll API calls (makes data loading deterministic)

10. Writing These Scenarios in Plain English with Assrt

Every scenario in this guide required careful coordination of scrolling, waiting, and DOM queries. The scroll-to-index test alone needed a helper utility, a timeout for settle, and knowledge of react-window internals. Assrt lets you express the same intent in plain English and compiles it into the exact Playwright TypeScript you would write by hand.

Here is the complete scroll-to-index scenario, the variable-height row verification, and the infinite scroll test compiled into a single .assrt file:

virtualized-list.assrt

Full Test Suite: Playwright vs Assrt

// 5 test files, ~350 lines total
// scroll-to-index.spec.ts (85 lines)
// variable-height.spec.ts (95 lines)
// infinite-scroll.spec.ts (90 lines)
// overscan.spec.ts (75 lines)
// keyboard-nav.spec.ts (65 lines)
// Plus helpers/scroll-utils.ts (50 lines)
// Total: ~400 lines of TypeScript
86% fewer lines

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When react-window updates its internal DOM structure or you migrate from react-window to react-virtuoso, Assrt detects the selector failures, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files stay untouched.

Start with the initial render verification. Once that passes in CI, add the scroll-to-index scenario, then variable-height rows, then infinite scroll, then keyboard navigation, then overscan tuning. In a single afternoon you can have complete virtualized list coverage that most applications never achieve by hand, and the tests will survive library upgrades without manual maintenance.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk