VALIDATION LAB•INFINITE SCROLL
Async
Infinite Scroll
Scroll a virtualized list until the target item appears, asserting the loader disappears between batches.
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.
123
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.
12
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
1234567891011121314
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.
infinite-scroll.spec.ts- Scrolling list
- Awaiting batch fetches
- Asserting item 42 visible