ScreenshotNeo

BlogHow-to

Selenium Snapshot Testing for Web Pages

Capture page and element screenshots with Selenium, compare them with reviewed baselines, and build a reliable visual regression workflow.

By the ScreenshotNeo team4 October 202612 min read

Selenium can capture screenshots of a page or a specific element, but it does not provide a complete snapshot-testing workflow by itself. To do visual regression testing, capture a known page state, compare the image with an accepted baseline using a comparison library or service, review any differences, and update the baseline only when the change is intentional.

This guide uses Python with Selenium and Pillow for a runnable local workflow, then explains how to make captures stable, handle common problems, and adapt the workflow to CI. Here, “snapshot” means an image. An accessibility or DOM snapshot is structured data and needs different assertions.

1. What Selenium snapshot testing covers

Selenium WebDriver controls a browser and can save a screenshot of the current browsing context or a WebElement. Screenshot capture is only one part of visual testing: Selenium does not decide which image is the baseline, how much difference is acceptable, or whether a change should be approved. Those responsibilities belong to your test code, comparison library, or visual-testing service.

Artifact What it checks Typical use
Viewport screenshot The visible browser area Page layout at a fixed viewport
Element screenshot A selected element A component, card, dialog, or chart
Full-page screenshot The page beyond the viewport Long-page appearance, subject to browser and capture-method limits
DOM or accessibility snapshot Structure, roles, names, or attributes Semantic and accessibility assertions rather than visual appearance

Selenium’s official WebDriver documentation describes page and element screenshot capture. Playwright documents ARIA snapshots separately from visual comparisons, which is a useful distinction: image diffs cannot tell you whether the accessible name or semantic structure is correct. See Selenium screenshot documentation, Playwright visual comparisons, and Playwright ARIA snapshots.

2. Install the local Python example

The example uses Selenium 4 and Pillow. Selenium Manager, included with modern Selenium, can manage a compatible browser driver in many common setups. Install the packages and ensure Chrome or Chromium is available:

python -m pip install selenium Pillow

Create a test page or choose a stable URL that your test environment controls. Do not rely on a production page whose content changes independently of your code.

3. Capture a deterministic screenshot

This script opens a page, sets a known viewport, waits for a target element, captures its screenshot, and compares it to an accepted PNG baseline. On the first run it writes a candidate image and exits with a message asking you to review and accept it. It never silently treats an unknown screenshot as an approved baseline.

from pathlib import Path
import sys

from PIL import Image, ImageChops
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

URL = "http://localhost:8000"
SELECTOR = "main"
BASELINE = Path("visual-baselines/home-main.png")
ACTUAL = Path("artifacts/home-main-actual.png")
DIFF = Path("artifacts/home-main-diff.png")

# Allow a small amount of per-channel pixel variation. Tune this only after
# confirming captures are deterministic in your CI environment.
MAX_DIFFERING_FRACTION = 0.001
CHANNEL_TOLERANCE = 8

options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
options.add_argument("--force-device-scale-factor=1")

driver = webdriver.Chrome(options=options)
try:
    driver.set_window_size(1440, 1000)
    driver.get(URL)

    wait = WebDriverWait(driver, 15)
    wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTOR)))
    # Add application-specific waits here for fonts, images, animations,
    # network-backed data, or a test-ready marker.

    element = driver.find_element(By.CSS_SELECTOR, SELECTOR)
    ACTUAL.parent.mkdir(parents=True, exist_ok=True)
    BASELINE.parent.mkdir(parents=True, exist_ok=True)
    element.screenshot(str(ACTUAL))
finally:
    driver.quit()

if not BASELINE.exists():
    print(f"No baseline exists. Review {ACTUAL}, then copy it to {BASELINE} deliberately.")
    sys.exit(2)

baseline = Image.open(BASELINE).convert("RGBA")
actual = Image.open(ACTUAL).convert("RGBA")
if baseline.size != actual.size:
    print(f"Size changed: baseline={baseline.size}, actual={actual.size}; inspect {ACTUAL}")
    sys.exit(1)

# Difference image makes failures reviewable. Count a pixel as changed if any
# color channel exceeds the chosen tolerance.
diff = ImageChops.difference(baseline, actual)
DIFF.parent.mkdir(parents=True, exist_ok=True)
diff.save(DIFF)
pixels = list(diff.getdata())
changed = sum(1 for pixel in pixels if max(pixel[:3]) > CHANNEL_TOLERANCE)
fraction = changed / (baseline.width * baseline.height)
print(f"Changed pixels: {changed} ({fraction:.4%}); diff: {DIFF}")
if fraction > MAX_DIFFERING_FRACTION:
    sys.exit(1)

Run your application locally, then run the script. Review artifacts/home-main-actual.png and the diff image. After confirming the captured state is the intended reference, create or update the baseline in a separate deliberate step:

mkdir -p visual-baselines
cp artifacts/home-main-actual.png visual-baselines/home-main.png

Commit the approved baseline with the test. In CI, fail when the comparison exceeds your policy and upload the actual and diff images as artifacts so the failure can be reviewed.

4. Capture the page, viewport, or one element

Capture the current viewport

Use the driver screenshot when the area visible in the browser is the intended scope:

driver.save_screenshot("artifacts/viewport.png")

In Python, this saves a PNG and returns a success value. Other bindings commonly offer a screenshot method returning Base64 data or writing to a file; check the binding’s Selenium API for its exact method and return type.

Capture a specific element

Wait for the element to be visible, locate it, and call its screenshot method:

element = WebDriverWait(driver, 15).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".pricing-card"))
)
element.screenshot("artifacts/pricing-card.png")

Element screenshots are often easier to stabilize and review than whole-page images. The element must be displayed and within a state the browser can render. A selector that matches multiple elements needs an explicit choice, such as selecting the first match or locating by a more specific parent.

Capture a full page

WebDriver’s ordinary screenshot captures the viewport. Full-page capture behavior is browser- and binding-dependent; Selenium’s page screenshot API should not be assumed to stitch an arbitrarily long page. Options include using a browser-specific full-page mechanism, scrolling and stitching images in your own code, or capturing a smaller element. If you implement stitching, account for sticky headers, lazy-loaded content, overlapping rows, and viewport-dependent layout. Keep the chosen method fixed across baseline creation and comparison.

5. Make the browser state reproducible

Most noisy visual tests are state-control problems. Before capturing, define the page state and environment rather than adding a permissive pixel threshold to hide instability.

  • Fix the viewport and scale. Set the same browser window dimensions and device scale factor for baseline and candidate captures.
  • Use stable data. Seed test records, freeze dates where possible, and avoid live prices, rotating promotions, random IDs, or personalized content.
  • Wait for the right condition. Wait for a meaningful element or application-ready marker. A fixed sleep can be too short on a slow run and unnecessarily long on a fast one.
  • Wait for images and fonts. A visible container does not guarantee that its fonts or image assets have finished loading. Add app-specific readiness checks where they matter.
  • Control animation and transitions. Prefer a test mode that disables animations or inject a narrowly scoped CSS override before capture. Avoid hiding large regions unless they are genuinely irrelevant to the assertion.
  • Control overlays. Dismiss consent dialogs and other overlays as a deliberate part of setup, or capture them as their own test case.
  • Use a consistent environment. Keep operating system, browser version, settings, hardware where practical, and headless mode consistent. Rendering can vary across these factors; Playwright’s visual comparison documentation explicitly warns about environment-dependent rendering.

Where you need custom setup, use Selenium’s script execution to set a test flag or inject a test-only style. Ensure the same setup is applied during baseline generation and candidate capture. Do not use setup code that changes the product behavior under test.

6. Compare images and manage baselines

The Pillow example uses a simple per-pixel threshold. This is easy to understand, but it treats all pixels equally and can be sensitive to anti-aliasing or rendering differences. Other approaches include strict pixel equality, a tolerated changed-pixel ratio, region masks, or perceptual comparison. Choose the comparison method based on the failures you need to catch, and document its tolerance.

Decision Practical guidance
Baseline location Store reviewed baselines with the test code or in a versioned artifact system. Make ownership and update history clear.
Approval Review actual and diff images before accepting a new baseline. An intentional redesign is a reason to update; a failing test alone is not.
Threshold Start strict in a stable environment. Relax only for understood rendering noise, and keep the threshold small and visible in code.
Scope Prefer the smallest region that fully tests the intended behavior. Use page-level captures when the page composition itself matters.
Cross-browser checks Maintain separate baselines for browser and platform combinations when their rendering differs. Avoid comparing one browser’s output to another browser’s baseline.
Failure artifacts Keep the baseline, actual image, and diff available in CI logs or downloadable artifacts.

A visual test is a review loop: capture a known state, compare to the accepted reference, inspect unexpected changes, then accept a changed reference only after review. Applitools describes visual testing as regression testing for screens that were previously correct; its documentation also describes baseline review workflows. A hosted visual-testing service is an option if a team needs managed review or broader visual-testing workflow features. Check the service’s current Selenium support and commercial terms directly before adopting it: Applitools documentation.

7. cURL, Python, and Node.js options

Selenium is available through language bindings. The Python example above is the complete comparison workflow. cURL is not a Selenium WebDriver binding; it can call a WebDriver-compatible remote endpoint only when that endpoint is configured to accept the relevant WebDriver protocol request. It does not itself launch or automate a local browser. The following examples show the common WebDriver session flow for an already-running remote endpoint with a valid session.

cURL: take a screenshot from a remote WebDriver session

WD="http://localhost:4444"
SESSION_ID="YOUR_SESSION_ID"

# The screenshot response contains Base64-encoded PNG data in the value field.
curl -sS "$WD/session/$SESSION_ID/screenshot" \
  | python -c 'import sys,json,base64; print(json.load(sys.stdin)["value"], end="")' \
  | base64 --decode > screenshot.png

This assumes a session already exists and that the endpoint implements the W3C WebDriver screenshot route. Session creation and capabilities depend on the remote grid or driver configuration. On Windows, use an equivalent Base64 decoder if the GNU base64 command is unavailable.

Python: standalone page screenshot

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait

options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")
    WebDriverWait(driver, 15).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )
    driver.save_screenshot("page.png")
finally:
    driver.quit()

For remote execution, configure the binding with the remote WebDriver URL and desired capabilities supported by your grid. The local Python comparison script in section 3 shows how to add an element wait and baseline comparison.

Node.js: standalone page screenshot

import { Builder, By, until } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome.js';

const options = new chrome.Options().addArguments(
  '--headless',
  '--window-size=1440,1000'
);
const driver = await new Builder()
  .forBrowser('chrome')
  .setChromeOptions(options)
  .build();

try {
  await driver.get('https://example.com');
  const main = await driver.wait(until.elementLocated(By.css('body')), 15000);
  await driver.wait(until.elementIsVisible(main), 15000);
  const base64Png = await driver.takeScreenshot();
  const fs = await import('node:fs/promises');
  await fs.writeFile('page.png', Buffer.from(base64Png, 'base64'));
} finally {
  await driver.quit();
}

Install dependencies with npm install selenium-webdriver and configure the matching Chrome browser and driver. The WebDriver screenshot string is Base64 encoded PNG data. Add an image-diff package or service separately for baseline comparison; the capture API alone does not perform that comparison.

8. CI, performance, reliability, and cost

CI workflow

  1. Install the pinned Selenium binding, browser, and required system dependencies.
  2. Run the application with deterministic test data and wait for a test-ready condition.
  3. Capture using the same viewport, scale, browser, and environment used to create baselines.
  4. Compare each candidate to its accepted baseline and fail when the documented policy is exceeded.
  5. Publish actual and diff images as build artifacts, and update baselines through a reviewed code change.

Performance and reliability

Browser startup and page loading usually dominate the cost of a small image comparison. Reuse a browser session for related captures when isolation allows, but keep test state independent and always quit the driver. Capture only the scope needed, avoid arbitrary long sleeps, and parallelize only when each worker has isolated browser profiles and test data. Excessive parallel browser sessions can increase memory use and make rendering less predictable.

For reliable diagnosis, log the URL, browser version, viewport, test data identifier, and relevant wait condition alongside the images. Save failure artifacts even when the test process exits unsuccessfully. Retry only failures known to be transient infrastructure issues; retries can conceal flaky state setup.

Cost

A local Selenium workflow has no per-screenshot API charge, but it uses CI compute, browser maintenance, storage for baselines and artifacts, and engineering time for review. Remote browser grids and hosted visual-testing services may have their own pricing and retention terms; check the current terms for the provider you choose. Keep screenshot retention proportionate to debugging needs.

9. Troubleshooting

Symptom Likely cause Fix
Driver cannot start or browser session creation fails Browser and driver mismatch, missing browser, or missing container dependencies Use a compatible browser and Selenium setup, inspect driver startup logs, and ensure required browser libraries are installed.
Screenshot is blank or incomplete Capture happened before the page or target rendered, or the wrong window/tab is active Switch to the intended browsing context, wait for a meaningful visible element, and verify the page state before capture.
Element screenshot raises an error Element is hidden, detached, covered, or not found by the selector Wait for visibility, use a stable selector, and reacquire the element after page updates.
Every run reports a visual difference Viewport, browser, OS, scale, fonts, dynamic data, or animation differs Align the environment and state first. Separate baselines per environment when differences are expected.
Image dimensions differ Window sizing or element layout changed, or the page rendered differently Check CSS breakpoints, browser chrome effects, device scale factor, and the actual image before considering a baseline update.
Intermittent diffs around text Font loading or anti-aliasing varies Wait for fonts, install the same fonts in CI, pin the environment, and avoid a broad threshold that could hide layout regressions.
Full-page image cuts off or repeats content Viewport capture was mistaken for full-page capture, or a scroll-and-stitch method mishandled sticky/lazy content Use a documented browser-specific method or capture a defined element; verify lazy content and sticky regions explicitly.
CI test passes locally but fails remotely Different browser build, OS, headless rendering, data, or timing Record environment metadata, align versions and data, and compare artifacts from both environments.
Baseline appears to update automatically Test code is writing candidate images over approved references Separate baseline and actual paths. Require an explicit review and update step.

10. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single GET request returns a screenshot as PNG, JPEG, or WebP, or a PDF. It can provide the candidate capture for a workflow, while you still manage and review visual baselines according to your team’s policy. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners are accepted and removed before capture; 60+ known consent platforms, newsletter popups, and chat widgets can be removed, and each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; responses identify the page verdict and billing status in headers.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

11. Frequently asked questions

Does Selenium include visual assertions?

Selenium provides browser automation and screenshot capture. You add image comparison and baseline review with your own code or a visual-testing tool.

Should I use a page or element screenshot?

Use an element capture when that component is the behavior under test. Use a page or viewport capture when layout relationships across the page matter.

Is an accessibility snapshot the same as a screenshot snapshot?

No. A screenshot is pixels; an accessibility snapshot is structured semantic information. Test both when both visual appearance and accessibility structure matter.

When should a baseline change?

After a reviewer confirms that the visual change is expected and the candidate represents the intended design in the test environment.