Skip to main content

How to Test Infinite Scroll and Pagination

Short answer

Infinite scroll fails on duplicate fetches, race conditions at scroll boundaries, and missing end-of-list states—not on whether more rows appear once. Seed paged APIs with known totals, wait for network responses or monotonic item counts, assert unique IDs, and probe cursor/page tokens instead of arbitrary scroll sleeps.

Part of Testing Guides by UI patterns.

Who this is for

Teams shipping feeds, activity timelines, chat history loaders, or product grids that append pages on scroll or "Load more" clicks. Typical stacks: cursor-based GraphQL, offset REST (?page=2), Intersection Observer hooks, React Virtualized lists.

Why testing infinite scroll matters

Scroll loaders look simple but break in costly ways:

  • Duplicate rows — double fetch at scroll threshold shows duplicate orders or charges in admin consoles.
  • Missing end state — users scroll forever into blank loaders; support tickets spike on "stuck loading."
  • Stale pages — filter change leaves page-3 cursor while UI shows page-1 items.
  • API cost — runaway prefetch in mobile webviews hammers backend; only visible at scale.
  • Accessibility — focus lost when new rows inject; keyboard users cannot reach appended items.

Assert monotonic unique IDs, terminal end state, and probe page metadata—not pixel scroll position.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Duplicate fetchScroll fires twiceDuplicate IDs in DOMAssert Set size equals row count
Fast scrollRequests overlapOut-of-order appendWait for response before next scroll
End of listNo sentinelInfinite spinnerAssert "end" probe or disabled loader
Empty first pageZero itemsLoader never stopsProbe total=0 upfront
Filter resetOld cursor reusedMixed itemsAssert cursor resets on filter
Scroll containerWindow vs divWrong scroll targetScroll locator not page
Load more buttonDisabled at endClick loopsAssert button hidden/disabled
Reverse chronologyPrepends vs appendsWrong orderProbe ordered IDs
Race on mountDouble initial fetchDuplicate page 1Network count assertion
Virtual listRows unmountedcount flakyProbe total loaded via API hook
Back navigationRestores scroll + stale dataWrong itemsbfcache or remount assert
Page size changeUser preference 25→50Gap in itemsProbe cumulative count

Seed paged API

// POST /api/test/seed-feed { runId, total: 47, pageSize: 10 }
await request.post('/api/test/seed-feed', { data: { runId, total: 47, pageSize: 10 } });

Known total lets you compute expected pages: ceil(47/10) = 5 loads, last page 7 items.

Scroll and wait pattern

const feed = page.getByTestId('scroll-feed');
async function loadNextPage() {
const before = await feed.getByTestId('feed-item').count();
const resp = page.waitForResponse(r => r.url().includes('/api/feed') && r.ok());
await feed.evaluate(el => { el.scrollTop = el.scrollHeight; });
await resp;
await expect.poll(() => feed.getByTestId('feed-item').count()).toBeGreaterThan(before);
return before;
}

let count = await feed.getByTestId('feed-item').count();
while (count < 47) {
count = await loadNextPage() + /* growth handled in poll */;
count = await feed.getByTestId('feed-item').count();
}
await expect(page.getByText(/end of feed|no more/i)).toBeVisible();

For cursor APIs, probe after each load:

const { nextCursor, items } = await request.get(`/api/test/feed-state?runId=${runId}`).then(r => r.json());
expect(nextCursor).toBeNull(); // terminal
expect(items.length).toBe(47);

Duplicate detection

const ids = await feed.getByTestId('feed-item').evaluateAll(nodes =>
nodes.map(n => n.getAttribute('data-id'))
);
expect(new Set(ids).size).toBe(ids.length);

Run after aggressive scroll jiggle tests that intentionally trigger double intersection events.

Pagination (numbered) variant

await page.getByRole('link', { name: '2' }).click();
await expect(page).toHaveURL(/page=2/);
const probe = await request.get(`/api/test/list?page=2&runId=${runId}`);
expect((await probe.json()).items).toHaveLength(10);

Numbered pagination shares filter-reset rules with infinite scroll—reset page index when filters change.

CI checklist

  1. Seed feeds with non-round totals (e.g., 47 items, page size 10)
  2. Scroll the correct container—log scrollHeight in debug on failure
  3. Wait for network or poll count—never fixed 2s sleep
  4. Assert unique IDs after stress scroll
  5. Terminal state spec mandatory
  6. Parallel workers use isolated runId feeds

Anti-patterns

Anti-patternWhy it failsBetter approach
window.scrollTo(99999) on div feedNo load triggeredScroll container element
Assert row count only onceMiss duplicatesUnique ID set
Skip end-of-listUX hang untestedProbe null cursor
Ignore filter+cursor resetMixed pagesReseed + assert page 1
Full 10k row scroll in CISlow, flakySeed small totals; unit test math
Screenshot-only assertMiss ID dupesProbe + Set uniqueness

Example scenario

Situation: User rapidly scrolls activity feed to bottom twice at network boundary.

Expected outcome: 47 unique events loaded; no duplicate IDs; end sentinel shown.

Why UI-only automation breaks: Row count reaches 47 but two rows share same event id—admin export double-counts.

  1. Arrange: Seed feed with 47 unique event ids for runId.
  2. Act: Scroll feed container with double intersection trigger at page boundaries.
  3. Assert: Probe items.length=47, nextCursor=null; DOM id Set size=47; end message visible.

TestChimp workflow: Optional: instrument feed_page_loaded with page_index; compare prod depth vs test when users routinely load 5+ pages.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I test infinite scroll without flaky scroll positions?

Scroll the feed container to scrollHeight, wait for the feed API response or poll item count until it increases, repeat until probe reports terminal cursor or expected total.

Window scroll vs scrollable div?

Most apps use an inner div—target that locator with evaluate scrollTop. Window scrollTo often does nothing and tests pass falsely.

How do I detect duplicate fetches?

After stress scrolling, collect data-id attributes from all rows and assert Set size equals array length. Optionally count network requests per page index.

Should I test numbered pagination separately?

Yes if your app supports both patterns—URL page param, probe page API, and filter reset behavior differ from cursor infinite scroll.

How do virtualized lists change assertions?

DOM row count may cap at viewport size—use probe API for total loaded items and spot-check visible rows via scrollIntoViewIfNeeded.

What end-of-list signal should I assert?

Prefer probe nextCursor=null or totalReached flag. UI may show text, hide loader, or disable button—probe is authoritative for data completeness.

When should TrueCoverage guide infinite scroll tests?

When prod users routinely load many pages but CI stops early—instrument pages_loaded and expand SmartTests for deep scroll paths.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo