VALIDATION LABINFINITE SCROLL

Async

Infinite Scroll

Scroll a virtualized list until the target item appears, asserting the loader disappears between batches.

Advanced

Scenario

Infinite scroll hides bugs in virtualization, intersection observers, and duplicate-key React warnings. The classic 'item 42 only appears on a second scroll' is a missed batch boundary.

Scroll to load more — total dataset is 60 items. Stable selectors: inf-item-N.

  • Defect #000
  • Defect #001
  • Defect #002
  • Defect #003
  • Defect #004
  • Defect #005
  • Defect #006
  • Defect #007
  • Defect #008
  • Defect #009
  • Defect #010
  • Defect #011

Live widget · interact freely

Manual test checklist

  • 1Scroll to the bottom — confirm a new batch loads
  • 2Scroll fast (flick) — does the loader appear AND disappear cleanly?
  • 3Reach the end of the list — confirm 'End of stream' shows, no more loader
  • 4Open DevTools — no React 'duplicate key' warnings in console
  • 5Resize the viewport — list re-virtualizes correctly

Expected result

Item #42 becomes visible after at least two batch fetches; loader spinner is removed after the final batch.

Automation challenge

Loop `scroller.evaluate(el => el.scrollBy(0, el.clientHeight))` until `getByTestId('inf-item-42')` is `visible`. Cap the loop at 20 iterations and `await page.waitForLoadState('networkidle')` between rounds — never `waitForTimeout`.

Stable selectors

  • Scroller[data-testid="inf-scroller"]
  • Item template[data-testid="inf-item-{n}"]
  • Loader[data-testid="inf-loader"]

Locator strategy

Three levels from simple IDs to scoped Playwright locators. IDs and names are easy to learn but are not always the best long-term choice when labels change or components repeat.

Use simple IDs and names to understand how locating elements works.

1
2
3
await page.goto('https://lab.hakdogan.com/practice/infinite-scroll');

await page.locator('#inf-scroller').evaluate((el) => el.scrollBy(0, 800));

Avoid as primary strategies

XPath (unless there is no alternative), long CSS chains, Tailwind-style utility class selectors, generated or unstable IDs, and volatile framework internals break when layout, styling, or DOM structure shifts.

1
2
await page.locator('.w-full.rounded-xl.border.bg-blue-500').fill('demo@example.com');
await page.locator('//div[2]/form/div[1]/input').fill('demo@example.com');

Reference Playwright spec

infinite-scroll.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { test, expect } from '@playwright/test';

test('infinite scroll reveals later item', async ({ page }) => {
  await page.goto('https://lab.hakdogan.com/practice/infinite-scroll');

  const scroller = page.getByTestId('inf-scroller');
  const target = page.getByTestId('inf-item-42');

  while (await target.count() === 0) {
    await scroller.evaluate((el) => el.scrollBy(0, el.clientHeight));
    await page.waitForTimeout(120);
  }
  await expect(target).toBeVisible();
});

Headed Test Playback

Simulated headed-browser flow — no real browser is launched.

Automation-style playback (Playwright-shaped logs). No real browser; no commands run on your machine.

Idleinfinite-scroll.spec.ts
Failure demos
https://lab.hakdogan.com/practice/infinite-scroll
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Scrolling list
  2. Awaiting batch fetches
  3. Asserting item 42 visible
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specinfinite-scroll.spec.ts