Playwright Full-Page Screenshots After Infinite Scroll
Load and verify every infinite-scroll batch before capturing a full-page Playwright screenshot. Use page-specific signals instead of relying on network idle.
To capture a complete Playwright screenshot of an infinite-scroll page, trigger each content load, wait for a page-specific signal that confirms the new items rendered, and repeat until the page’s end condition is met. Then use await page.screenshot({ path: 'full-page.png', fullPage: true }). The fullPage option captures the full scrollable page; it does not establish that an infinite list has finished loading.
This guide uses JavaScript with Playwright’s test runner. Adapt the selectors, response predicate, and stopping condition to the target site. [Playwright Page API]
1. Install Playwright and identify the page’s signals
In a new project, install Playwright Test and its browser:
npm init playwright@latest
npx playwright install chromium
Before writing the loop, inspect the page in a browser’s developer tools or in your test to answer these questions:
- What triggers another batch: scrolling near the last card, clicking a “Load more” button, or another action?
- Which selector identifies the rendered items?
- Can you identify the request that fetches each batch, and a stable URL or property that matches it?
- How does the page indicate loading and completion? Look for a loading indicator, a total count, a “no more results” marker, or a known final item.
A successful response shows that a request completed, but it does not always prove that the response’s contents have been rendered. When possible, verify both the request and an observable DOM change.
2. Use bounded, page-specific waits
Here is a runnable Playwright Test example. Replace the example URL and selectors, and tailor isBatchResponse to the site’s actual API. This version scrolls the last rendered item into view, waits for the matching response and a larger item count, and stops when it sees an end marker. It also limits the number of batches so an unexpected page behavior cannot cause an unbounded loop.
import { test, expect } from '@playwright/test';
test('capture all loaded results', async ({ page }) => {
const url = 'https://example.com/results';
const itemSelector = '[data-testid="result-card"]';
const endSelector = '[data-testid="end-of-results"]';
const maxBatches = 30;
await page.goto(url, { waitUntil: 'domcontentloaded' });
await expect(page.locator(itemSelector).first()).toBeVisible();
for (let batch = 0; batch < maxBatches; batch++) {
if (await page.locator(endSelector).isVisible().catch(() => false)) {
break;
}
const before = await page.locator(itemSelector).count();
const lastItem = page.locator(itemSelector).last();
// Register the wait before scrolling because scrolling may trigger the request.
const responsePromise = page.waitForResponse(
response => {
const requestUrl = response.url();
return requestUrl.includes('/api/results') && response.ok();
},
{ timeout: 15000 }
);
await lastItem.scrollIntoViewIfNeeded();
// If the site can legitimately make no request at the end, handle that
// site-specific condition explicitly instead of treating every timeout as success.
await responsePromise;
// Confirm that the response produced more rendered items.
await expect(page.locator(itemSelector)).toHaveCountGreaterThan(before, {
timeout: 10000
});
}
// Check the end condition here if the site provides one. If the cap was reached
// without completion, fail rather than silently saving an incomplete capture.
await expect(page.locator(endSelector)).toBeVisible({ timeout: 1000 });
await page.screenshot({ path: 'full-page.png', fullPage: true });
});
toHaveCountGreaterThan is not a built-in Playwright assertion. For a fully runnable test, replace that line with a supported assertion over a page-specific signal. One option is a predicate wait:
await expect.poll(() => page.locator(itemSelector).count(), {
timeout: 10000
}).toBeGreaterThan(before);
Use that replacement in the example above. If a batch can legitimately return zero new items, adjust the logic to recognize the site’s end marker or empty response instead of requiring the count to increase. A response matcher should be narrow enough to avoid matching unrelated requests, and it may need to allow expected non-success statuses if the site’s API uses them to signal completion.
The test above intentionally fails if it reaches the batch cap without seeing the end marker. If the site exposes a total count instead, compare the rendered count to that value and use it as the completion check. If there is no explicit end signal, define a site-appropriate rule—such as a confirmed empty batch—and retain a hard batch or time limit.
Playwright supports waiting for a matching response and for page-specific selector or text conditions. [Playwright waiting guidance]
3. Choose a completion signal that matches the page
| Signal | What it establishes | Best use |
|---|---|---|
| Matching successful response | The relevant request returned successfully | The page has a recognizable API request; pair it with a DOM check when possible |
| Item count increases | More items appeared in the DOM | Each batch adds identifiable cards or rows |
| Loading indicator disappears | The page left its loading state | The indicator reliably covers the request and rendering phase |
| Known total is reached | The expected number of items is present | The page exposes a trustworthy total count |
| End marker appears | The page indicates there are no more items | The site has a stable end-of-list marker |
Prefer a signal tied to the content you need. A generic network quiet period can be misleading: background traffic may continue after the list is ready, or the network can be quiet before another scroll-triggered batch begins. Those are practical reasons to use page-specific signals.
4. Handle buttons, lazy images, and unusual list behavior
If the page has a “Load more” button
Click the button instead of scrolling. Register the response wait before the click, then verify a count change or an end marker. If the button disappears at completion, treat that as the stopping signal only if the site consistently uses that behavior.
const loadMore = page.getByRole('button', { name: 'Load more' });
const before = await page.locator(itemSelector).count();
const responsePromise = page.waitForResponse(r =>
r.url().includes('/api/results') && r.ok()
);
await loadMore.click();
await responsePromise;
await expect.poll(() => page.locator(itemSelector).count())
.toBeGreaterThan(before);
If cards load but their images do not
Infinite-scroll completion and image completion are different conditions. After all expected cards are rendered, wait for the images you care about to finish loading. For example, inspect each image’s complete and naturalWidth properties, or wait for a site-specific image-ready marker. Lazy images may only start loading when brought near the viewport, so scrolling through the list before capture can help trigger them. Avoid assuming that an item count proves every image is ready.
If the page recycles DOM nodes
Some virtualized lists keep only visible rows in the DOM and replace them as you scroll. In that case, the DOM count will stay constant. Track a stable item identifier, capture each batch as it appears, or use the page’s total and end signals. A single full-page screenshot may not include off-screen content that the page never keeps in the rendered document; inspect the result and choose a capture strategy that matches the site’s rendering model.
If a batch can contain no new items
Do not wait forever for the item count to increase. Check whether the response represents an empty final batch, whether the end marker appeared, or whether a page-specific loading state finished. Make the terminal condition explicit and distinguish it from a failed request.
5. Capture the full scrollable page
Once loading is complete and the end condition has been verified, take the full-page screenshot:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Playwright documents fullPage: true as capturing the full scrollable page rather than only the visible viewport. That setting describes the screenshot region; it does not promise to trigger every site’s infinite scrolling or lazy loading. [Page screenshot API]
For test output, use Playwright’s snapshot assertion when you want visual comparison:
await expect(page).toHaveScreenshot('results.png', { fullPage: true });
Screenshot rendering can vary across operating systems, browser versions, settings, hardware, power source, and headless mode. Keep the environment consistent for useful comparisons. Playwright’s screenshot assertion waits for two consecutive page screenshots to match before comparing. [Visual comparisons]
6. Why not just wait for network idle?
Playwright defines networkidle as having no network connections for at least 500 ms, and its documentation discourages using that state as a general readiness check. An infinite-scroll page may not request its next batch until you scroll, so network quiet before that action says nothing about the remaining list. Background requests can also make network activity a poor proxy for whether the content you need has rendered. [Page API and wait states]
Use networkidle only when that network condition itself is relevant to your task. For a complete list capture, trigger the load and wait for the list’s own response, DOM change, loading transition, count, or end marker.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot has only the first batch | fullPage captured the current document before more batches were triggered |
Run the trigger-and-confirm loop to the page’s completion condition before capture |
| The response wait times out | The URL predicate missed the request, scrolling did not trigger it, or the page reached the end | Inspect the request URL and trigger behavior; handle a confirmed terminal state separately from an error |
| The request succeeds but the count does not change | Rendering is delayed, the response added no items, or the list virtualizes its DOM | Wait for a loading transition or stable item identifier; use the site’s end condition for empty batches |
| The loop never terminates | The end selector is wrong or the page lacks that marker | Verify the selector and use a known total or terminal response; keep a maximum iteration or time bound |
| Images or sections are missing | Lazy content was not triggered or image loading was not included in the completion check | Scroll through relevant content and wait for image readiness separately |
| Screenshot comparison is flaky | The rendering environment or page content changes between runs | Keep browser and host settings stable and control changing content where possible |
| Navigation hangs on a broad wait condition | The page keeps background network activity active | Use a narrower navigation condition such as domcontentloaded, then wait for the specific content state |
8. Performance, reliability, and cost
Every additional batch adds request, rendering, and screenshot work. Use the smallest reliable completion checks: match the relevant request, assert a meaningful DOM change, and stop at a known end condition. Bound the number of iterations and each wait. A timeout should produce a diagnostic failure, not silently save a partial image.
Very long pages can take longer to render and produce large image files. If the target is enormous, consider whether the task needs one full-page image or separate captures for each section. For screenshot tests, stabilize the browser environment and account for dynamic page content before comparing images. The cited Playwright documentation does not specify a universal maximum page length or capture-time benchmark, so measure against the actual page and environment.
Playwright is browser automation software; this workflow has no per-screenshot ScreenshotNeo charge. Your practical costs are the compute, runtime, and storage used by your own environment. The ScreenshotNeo option below has published monthly plan limits and prices for API captures.
Or skip the browser setup
For a normal page capture without manually running a browser loop, ScreenshotNeo provides a screenshot API and MCP server for developers. One GET request returns an image or PDF. For this page, adapt the URL in the request and consult the ScreenshotNeo API docs for parameters and response details.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/results \
-o results.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/results"},
timeout=90,
)
open("results.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/results'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('results.webp', bytes));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. A screenshot API call does not replace a page-specific loop when the task requires proving that a particular infinite-scroll list reached its true end.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does fullPage: true trigger infinite scrolling?
No. It captures the full scrollable page; trigger and verify additional content loads first.
Is a successful API response enough to take the screenshot?
It confirms the matching request succeeded. If the screenshot depends on rendered content, also wait for a DOM or page-state signal.
What if the site has no end marker?
Use another verifiable terminal condition, such as a known total or a confirmed empty final batch, and keep a hard loop limit.
Can a virtualized list be captured as one full-page image?
Not necessarily. If off-screen rows are removed from the DOM as you scroll, use a capture strategy that records each batch or changes how the page renders before taking a full-page screenshot.


