Resource library

QA How-To

How to Test an infinite scroll in Playwright (2026)

Learn playwright how to test an infinite scroll with deterministic scrolling, response waits, item-count assertions, termination checks, and TypeScript.

18 min read | 3,532 words

TL;DR

Scroll a specific container or the last visible item, synchronize on an observable result such as a matching response or increased item count, and stop with an explicit limit. Assert content identity and the terminal state, not only that the scroll command ran.

Key Takeaways

  • Scroll a specific container or the last visible item, synchronize on an observable result such as a matching response or increased item count, and stop with an explicit limit. Assert content identity and the terminal state, not only that the scroll command ran.
  • Prefer web-first assertions over manual polling where possible.
  • Create event and response waits before the action that triggers them.
  • Use accessible locators and stable product contracts.
  • Keep loops and waits bounded for CI safety.
  • Retain traces and targeted artifacts for useful failure diagnosis.

Scroll a specific container or the last visible item, synchronize on an observable result such as a matching response or increased item count, and stop with an explicit limit. Assert content identity and the terminal state, not only that the scroll command ran. This guide answers playwright how to test an infinite scroll with production-oriented Playwright Test patterns for TypeScript.

The examples focus on reliable synchronization, accessible locators, useful assertions, and diagnostics. Replace the illustrative URLs and accessible names with contracts from your application, while keeping the event ordering and assertion strategy intact.

TL;DR

Decision Recommended approach Why
scrollIntoViewIfNeeded Item sentinel Intent is clear and localized
mouse.wheel Window or realistic gesture Needs a focused scroll target
DOM scrollTop evaluation Custom container Use only when user-level actions cannot reach it

The core rule is simple: observe the outcome that matters, start event waits before the triggering action, and let Playwright's locator assertions retry. Never repair uncertainty with a fixed sleep.

1. playwright how to test an infinite scroll: Define the infinite-scroll contract

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

2. Choose the real scroll container

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

3. Trigger one deterministic load

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

4. Synchronize with network or UI

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

5. Assert new unique content

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

6. Use a bounded loading loop

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

7. Verify the end-of-feed state

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

8. Test errors and retries

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

9. Handle virtualized lists

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

10. Measure accessibility behavior

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

11. Debug scroll flakiness

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

12. Apply playwright how to test an infinite scroll

Testing infinite scrolling and progressively loaded feeds starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.scrollIntoViewIfNeeded(), mouse.wheel(), page.waitForResponse(), expect.poll(), locator counts, and bounded loops. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

Runnable TypeScript example

import { test, expect } from '@playwright/test';

test('loads the next feed page', async ({ page }) => {
  await page.goto('https://example.com/feed');
  const cards = page.getByTestId('feed-card');
  const before = await cards.count();
  const responsePromise = page.waitForResponse(response =>
    response.url().includes('/api/feed') && response.request().method() === 'GET'
  );
  await cards.last().scrollIntoViewIfNeeded();
  const response = await responsePromise;
  expect(response.ok()).toBeTruthy();
  await expect.poll(() => cards.count()).toBeGreaterThan(before);
});

Run the example with npx playwright test. For framework setup, review the Playwright TypeScript framework guide, then strengthen locator choices with Playwright locator best practices. Teams troubleshooting intermittent runs can also use the Playwright flaky test guide.

Interview Questions and Answers

Q: What is the first principle you would explain for infinite scrolling and progressively loaded feeds?

I would define an observable contract before selecting an API. The action alone is not proof. I would assert the state a user or downstream system depends on, using a locator or response that Playwright can retry safely.

Q: Why should a test avoid waitForTimeout()?

A fixed delay guesses how long the application needs. It wastes time when the UI is fast and still fails when CI is slower. I wait for a precise event, response, locator state, or polled condition with a meaningful timeout.

Q: How do you prevent races?

I create the promise for a relevant event or response before performing the action that can emit it. Then I await the action and the promise in a clear order. This prevents the application from completing before the listener exists.

Q: Which locators are most maintainable?

I prefer roles, accessible names, labels, and stable test IDs. These represent user-facing contracts and are less coupled to DOM structure. I use CSS only where it expresses a stable product contract.

Q: What belongs in a failure report?

The report should show the expected contract, actual state, relevant URL, and diagnostic artifacts. A retained trace is often more useful than a screenshot alone because it includes actions, DOM snapshots, console messages, and network activity.

Q: How would you make this coverage CI-safe?

I isolate data, bound waits and loops, avoid order dependence, and run against a controlled environment. I retain artifacts on failure and validate the same test under configured browser projects without adding browser-specific sleeps.

Common Mistakes

  • Waiting a fixed number of milliseconds instead of waiting for evidence.
  • Using brittle selectors tied to layout or generated classes.
  • Checking only that an action succeeded, without asserting business state.
  • Creating an event or response wait after the triggering action.
  • Allowing unbounded loops that can hang a worker.
  • Sharing mutable state between parallel tests.
  • Hiding a product defect with retries or broad exception handling.
  • Capturing secrets or customer data in traces and screenshots.

Treat each mistake as a review prompt. A stable suite is not one that never fails. It is one whose failures are reproducible, correctly classified, and rich enough to diagnose. Review helper abstractions periodically because a helper that hides event ownership or swallows assertions makes failures harder to understand.

Conclusion

The practical answer to playwright how to test an infinite scroll is to use Playwright's supported APIs, synchronize on observable outcomes, and assert the product contract with retrying locators. The exact command matters, but event ordering, isolation, and a meaningful final assertion matter more.

Start with one representative happy-path test, add a negative or boundary case, and run both repeatedly in the same CI configuration used by the team. Once those tests are trustworthy, extract only the repeated mechanics into a small helper or fixture.

Interview Questions and Answers

How would you design this test?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you avoid flaky synchronization?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

Which locator strategy would you choose?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you handle negative paths?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

What diagnostics do you retain?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you keep the test maintainable?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

Frequently Asked Questions

What is the recommended starting point for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Should I use a fixed timeout for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

How should I choose locators for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

How do I debug a CI-only failure for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Should I create a reusable helper for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

What should the final assertion prove for infinite scrolling and progressively loaded feeds?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Related Guides