Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To test scrolling with pytest and Playwright, choose the action that matches the behavior: use scroll_into_view_if_needed() to bring a target into view, page.mouse.wheel() to test a user’s wheel gesture, or locator.evaluate() to scroll a specific nested container. Then assert an observable result—such as a newly loaded item, a visible end marker, or a changed UI state—not merely that the scroll command ran.

Set up pytest and Playwright

The official Playwright introduction recommends its pytest plugin for Python browser tests. The plugin supplies browser and page fixtures, so a test can accept page as an argument without creating a browser manually. Playwright Python supports synchronous and asynchronous APIs and can run tests locally or in CI with Chromium, WebKit, and Firefox. See the Playwright Python introduction and the pytest 8.3 documentation.

Install the plugin and browser binaries in your project environment:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
python -m pip install pytest-playwright
playwright install

A synchronous test can use a typed page fixture and Playwright’s retrying assertion:

from playwright.sync_api import Page, expect


def test_footer_becomes_visible(page: Page):
    page.goto("https://example.test/long-page")
    footer = page.get_by_role("contentinfo")
    footer.scroll_into_view_if_needed()
    expect(footer).to_be_visible()

Replace the example URL and target with elements from your application. A successful scroll assertion should describe what the user or application can observe after scrolling.

Choose the scrolling method that matches the test

Method Best for What it controls
locator.scroll_into_view_if_needed() Reaching a known element or triggering content near a sentinel Target visibility; Playwright scrolls only if needed
page.mouse.wheel(delta_x, delta_y) Testing wheel input and its handling by the interface A simulated wheel gesture at the pointer location
locator.evaluate(...) Scrolling a particular nested panel or setting precise container movement The selected DOM element’s scroll position

These methods test different things. Use the target-based method when the endpoint matters, wheel input when the gesture matters, and direct container scrolling when a nested element—not the document—is the subject of the test.

Scroll a target into view

Playwright’s scrolling guide notes that it will usually scroll automatically before an action. For an explicit scroll test, call scroll_into_view_if_needed() on a locator. It waits for actionability checks and scrolls the element into view unless it is already completely visible according to IntersectionObserver visibility. The guide’s example uses text in a footer; this method can also bring an infinite-list sentinel into view. Read the Playwright scrolling guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.sync_api import Page, expect


def test_infinite_list_loads_more(page: Page):
    page.goto("https://example.test/feed")
    items = page.get_by_role("listitem")
    before = items.count()

    page.get_by_test_id("feed-footer").scroll_into_view_if_needed()

    expect(items).to_have_count(before + 20)

The count change is an example contract, not a universal batch size. Replace before + 20 with the application’s actual expected result. Other useful outcomes include a new card appearing, a loading indicator disappearing, or a “no more results” marker becoming visible. Prefer such observable conditions to a fixed sleep: applications load at different speeds, and Playwright documentation specifies no universal sleep duration or scroll distance.

Test a user’s wheel gesture

Use page.mouse.wheel(delta_x, delta_y) when the interaction itself is important—for example, when testing a reader panel’s response to wheel input. Hover the intended surface first so the pointer is over the area that should receive the gesture. Playwright’s documented example follows this pattern. The delta is application-specific; there is no universal value that guarantees a particular amount of movement.

from playwright.sync_api import Page, expect


def test_wheel_reaches_next_section(page: Page):
    page.goto("https://example.test/reader")
    panel = page.get_by_test_id("scrolling-container")
    panel.hover()
    page.mouse.wheel(0, 600)

    expect(page.get_by_role("heading", name="Chapter 2")).to_be_visible()

Choose a direction and delta appropriate for your interface, then assert the resulting state. A wheel call alone proves only that the test issued an input event; it does not prove that content loaded, that the intended panel moved, or that the user reached the expected section.

Scroll a nested div or virtualized panel

If a dashboard has an independently scrollable region, directly update that element’s scrollTop through its locator. This avoids moving the document viewport by mistake. Playwright documents the pattern e => e.scrollTop += 100; adjust the amount for your UI rather than treating it as a standard distance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.sync_api import Page, expect


def test_inner_panel_scrolls(page: Page):
    page.goto("https://example.test/dashboard")
    panel = page.get_by_test_id("scrolling-container")
    panel.evaluate("e => e.scrollTop += 300")

    expect(page.get_by_test_id("panel-end-marker")).to_be_visible()

For virtualized lists, the DOM may contain only currently rendered rows, so a raw item count or a particular scrollTop value may not prove that the intended data loaded. Prefer an application-facing marker, row text, or loaded-result count that represents the behavior under test.

Check whether an element requires scrolling to reach

Ordinary locator actions automatically scroll targets into view when needed. That default is helpful for normal interaction tests, but it can hide a bug if the requirement is that a user must scroll before an element is actionable. Playwright supports scroll: "none" on actions such as click() to disable that automatic step. Use this deliberately and make the expected non-actionability part of the test.

import pytest
from playwright.sync_api import Page


def test_continue_requires_scroll(page: Page):
    page.goto("https://example.test/long-page")
    button = page.get_by_role("button", name="Continue")

    with pytest.raises(Exception):
        button.click(scroll="none", timeout=1000)

This sketch illustrates the behavior but catches a broad exception. In production tests, narrow the expected error to the Playwright exception type appropriate to your installed version, so unrelated failures do not pass the test. You can also verify a disabled state or another explicit application rule before scrolling, then perform the ordinary action after reaching the control.

Keep the timeout short only when the expected outcome is a failed action; the value above is an example, not a general recommendation. For ordinary interactions, retain automatic scrolling: it is usually more stable than requiring a particular viewport position.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use resilient locators and assertions

Playwright describes locators as central to its auto-waiting and retryability model. Its locator guide recommends user-facing locators such as role, text, label, placeholder, alt text, title, and test ID. Long CSS or XPath chains tied to the current DOM structure are brittle; use them only when that structure is itself part of the contract. See the Playwright locator guide.

load_more = page.get_by_role("button", name="Load more")
panel = page.get_by_test_id("scrolling-container")
footer_text = page.get_by_text("Footer text")

For scrolling tests, keep the locator anchored to the meaningful target or container, then assert a UI result with Playwright’s expect helpers. Assertions that wait for the expected UI state are generally more robust than reading a value immediately after input and assuming all application work has finished.

Use the async API when the test suite is async

Playwright Python provides both sync and async APIs. The same test intent applies in either style; async code awaits navigation, scrolling, and assertions. Use the async fixture and test conventions supported by your project rather than mixing sync objects into async tests.

import pytest
from playwright.async_api import Page, expect


@pytest.mark.asyncio
async def test_footer_becomes_visible(page: Page):
    await page.goto("https://example.test/long-page")
    footer = page.get_by_role("contentinfo")
    await footer.scroll_into_view_if_needed()
    await expect(footer).to_be_visible()
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Run locally and in CI

The pytest plugin integrates browser fixtures into pytest. A basic local run is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
pytest

Playwright supports Chromium, WebKit, and Firefox for local and CI execution. Run against the browsers your application supports and your test environment installs. Differences in rendering, browser behavior, or timing can expose issues that a single-browser run will miss; no official Playwright requirement says that every project must run every browser on every test invocation.

Troubleshoot scrolling tests

  • The target stays off-screen. Confirm the locator matches the intended element and that the page finished navigating to the expected route. For an endpoint test, call scroll_into_view_if_needed(); for a gesture test, hover the intended surface before using the wheel.
  • The page moves instead of the nested panel. Target the panel locator and adjust its scrollTop with evaluate(), or make sure the pointer is over the nested panel before sending wheel input.
  • An infinite list does not add results. Scrolling may not be the only condition for loading. Verify the sentinel is correct and assert the application’s actual load result, such as new content or a completion marker; avoid assuming a fixed batch size.
  • The test passes without proving the scroll behavior. A successful call is not an outcome. Assert a visible target, loaded content, marker, or another state change tied to the requirement.
  • A “must scroll first” test passes unexpectedly. Locator actions scroll automatically by default. Disable scrolling with scroll="none" for the reachability check and assert the expected non-actionability rather than allowing any exception to count as success.
  • The selector breaks after a layout change. Replace DOM-position-dependent CSS or XPath chains with a role, accessible name, label, text, or test ID where possible.
  • A fixed delay works locally but flakes in CI. Replace arbitrary sleeps with an assertion about the loaded or visible state. There is no one sleep duration or pixel distance that works across applications.

Or skip the browser setup

If your goal is a screenshot rather than a test of scrolling behavior, ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request captures a URL as PNG, JPEG, WebP, or PDF. It is not a substitute for pytest assertions about wheel input, nested scrolling, or reachability; use Playwright for those interaction tests.

For a quick screenshot, install Python’s requests package and run:

import requests

r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.test/long-page"}, timeout=90)
open("shot.webp", "wb").write(r.content)

See the ScreenshotNeo API documentation for the request and available options. Cookie banners are accepted and removed before capture, along with supported newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

Frequently Asked Questions

Does Playwright automatically scroll before clicking?

Usually. Locator actions scroll targets into view by default; use scroll="none" when the test specifically needs to avoid that behavior.

Should I use a fixed sleep after scrolling?

Prefer a retrying assertion for the visible or loaded application state. A fixed delay is not universal across pages and environments.

Can Playwright test scrolling in a nested div?

Yes. Use a locator for that container and adjust its scrollTop with locator.evaluate(), then assert a meaningful result.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.