Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
A Selenium screenshot can be a perfectly valid PNG and still show the wrong visual state. The capture records whatever the browser has rendered at that instant; it does not prove that JavaScript, fonts, lazy images, animations, canvas content, or a post-click network request have finished. document.readyState === 'complete' covers assets declared in the HTML, while scripts can continue changing the page.
The reliable fix is to wait for an application-specific readiness condition, verify fonts and media, freeze or await motion, confirm the intended window and frame, and validate the image itself after saving.
What a “false” Selenium screenshot actually means
“False” does not normally mean Selenium fabricated pixels. It means the pixels are from an intermediate, stale, incomplete, or unintended browser state. Common examples include:
- A single-page application has finished navigation, but its API response has not rendered.
- A click started a transition and the screenshot caught the old panel or halfway-faded component.
- A web font arrived after the screenshot, changing line breaks and element positions.
- An intersection observer has not loaded images below the fold.
- The driver captured the current viewport or frame when the test author expected the full document.
- The file was written successfully, but to an unexpected path or over an earlier artifact.
Selenium’s waits documentation warns that readyState only concerns assets declared in HTML; loaded JavaScript can continue changing the site. Treat page readiness and visual readiness as separate states.
#1 Best Overall
Use an explicit visual-readiness contract
Replace guessed sleeps with conditions that represent the state your test needs. Selenium repeatedly evaluates an explicit-wait condition until it is truthy or the timeout expires.
Signals worth waiting for
- Application marker: a result element is visible, an expected heading contains the new value, or a stable status attribute appears.
- Loading completion: a known spinner, skeleton, or loading mask is hidden or removed.
- Stable content: the element’s text, class, bounding rectangle, or computed style has the expected value.
- Fonts:
document.fonts.readyresolves before layout-sensitive capture. - Images: relevant images are complete and have nonzero natural dimensions.
- Custom rendering: the application sets a “render complete” flag after canvas or WebGL drawing.
A robust condition can combine several signals instead of trusting one generic event.
Why fixed sleeps are a poor substitute
| Approach | Determinism | Suite time | Failure diagnosis |
|---|---|---|---|
| Fixed sleep | Still races when a slow request exceeds the delay | Always pays the full delay, even on fast runs | Usually reports only that the screenshot differs |
| Explicit application condition | Waits for the state that matters | Returns as soon as the condition is true | Timeout identifies the missing readiness signal |
A deterministic Python capture sequence
The following example navigates, performs a click, waits for an application marker and hidden loading mask, waits for fonts and images, checks that geometry is stable across two polls, then saves and validates the PNG. Adapt selectors and expected text to your application.
from pathlib import Path
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
URL = 'https://example.com/dashboard'
OUT = Path('/absolute/path/artifacts/dashboard.png')
options = webdriver.ChromeOptions()
options.add_argument('--headless=new')
options.add_argument('--window-size=1440,1000')
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 30, poll_frequency=0.2)
try:
driver.get(URL)
# Replace these selectors with your app’s real readiness contract.
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '[data-testid="dashboard"]')))
wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '[data-testid="status"]'), 'Loaded'))
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '[data-testid="loading-mask"]')))
# Trigger the state you intend to capture, if required.
# wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-action="refresh"]'))).click()
# Wait for web fonts.
wait.until(lambda d: d.execute_script("return document.fonts ? document.fonts.status === 'loaded' : true"))
# Wait for images that are already in the DOM.
wait.until(lambda d: d.execute_script("""
return Array.from(document.images).every(img =>
img.complete && (img.naturalWidth > 0 || img.src.startsWith('data:'))
);
"""))
# Require two identical layout samples, 200 ms apart.
def layout_is_stable(d):
signature = d.execute_script("""
const root = document.querySelector('[data-testid="dashboard"]');
if (!root) return null;
const r = root.getBoundingClientRect();
return [r.x, r.y, r.width, r.height,
getComputedStyle(root).opacity,
getComputedStyle(root).transform].join('|');
""")
if not hasattr(layout_is_stable, 'previous'):
layout_is_stable.previous = (None, 0)
previous, count = layout_is_stable.previous
count = count + 1 if signature == previous and signature else 0
layout_is_stable.previous = (signature, count)
return count >= 1
wait.until(layout_is_stable)
driver.switch_to.default_content()
driver.execute_script('window.scrollTo(0, 0)')
OUT.parent.mkdir(parents=True, exist_ok=True)
ok = driver.save_screenshot(str(OUT))
if not ok or not OUT.exists() or OUT.stat().st_size == 0:
raise RuntimeError('Screenshot was not written correctly')
print(f'{OUT.resolve()} ({OUT.stat().st_size} bytes)')
finally:
driver.quit()
save_screenshot returning true is an I/O result, not a visual assertion. Open the image or inspect it with an image library in your test pipeline; verify dimensions, nonzero byte size, and (for regression tests) the expected pixels.
Waiting after a click
Wait for the old state to disappear and the new state to appear. For example, wait for a progress indicator to become invisible, then wait for a result heading to contain the new identifier. If a click changes a route in an SPA, wait for the route-specific marker rather than merely checking that the URL changed.
Fonts, images, animation, canvas and WebGL
Fonts and layout shift
Fonts can load asynchronously after WebDriver considers navigation complete. A late font changes glyph widths, wrapping, and element coordinates. Wait for document.fonts.ready or your framework’s equivalent, then verify a layout signature remains unchanged. WebdriverIO’s visual-testing documentation calls out this asynchronous font behavior and waits for fonts by default in its visual workflow.
Rank #2
Animations and transitions
A screenshot during a transition can contain an intermediate opacity, position, or size. Prefer a test stylesheet that disables transitions and animations when product behavior allows it. Otherwise wait for the transition’s final class or computed style and require stable geometry across successive polls. Do not rely on a delay that happens to match one machine’s animation speed.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsLazy-loaded images
Ordinary DOM visibility does not guarantee that an image loaded by an intersection observer is ready. Scroll the target into its intended viewport if necessary, then wait for img.complete and a nonzero naturalWidth. A page-level “all images complete” check should be narrowed when third-party images are optional or intentionally broken.
Canvas and WebGL
Canvas and WebGL pixels may be produced outside the DOM conditions your wait observes. Add an application-level flag, such as a data attribute set after the final draw call, and wait for that flag. If the rendering is time-dependent, freeze the clock or use deterministic input where your application supports it.
Make sure Selenium captured the intended surface
Screenshot behavior is implementation-scoped. Selenium documents a best-effort order: an implementation may capture the entire page, the current window, the visible frame, or— for a non-conforming implementation—the entire display. Confirm what your browser and driver actually support before treating a result as a full-page baseline.
Check context before capture
- Window: record the current window handle and switch to the handle opened by the action if a new tab was expected.
- Frame: switch into the intended iframe, or return to
default_content()for the top document. - Viewport: set an explicit width and height; do not let a shared worker inherit an arbitrary window size.
- Scroll: set a known scroll position for viewport screenshots and use a driver-supported full-page method when the entire document is required.
- Device scale factor: keep the same DPR or browser configuration between baseline and comparison runs.
An element screenshot, viewport screenshot, and full-page screenshot answer different questions. Select the smallest scope that proves the behavior you are testing.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstall| Capture scope | Use it for | Typical risk |
|---|---|---|
| Element | Component-level assertions | Missing surrounding context or clipped overflow |
| Viewport | What a user sees at a fixed scroll position | Below-the-fold content is absent |
| Full page | Document-wide visual baselines | Driver support and lazy-loading behavior vary |
Validate the artifact, not just the write operation
Record the absolute path, timestamp, byte size, image dimensions, browser and driver versions, viewport, DPR, headless mode, window handle, frame, and scroll position. These fields turn “the screenshot is wrong” into a diagnosable event.
Rank #3
- Confirm the file exists where the test reports it.
- Reject zero-byte files and unexpectedly tiny dimensions.
- Use a unique artifact name or clean the destination so an old PNG cannot be mistaken for the current run.
- Retain the actual image and readiness diagnostics when a visual assertion fails.
For visual comparisons, pin the operating system, browser and driver versions, browser settings, hardware class, power mode, viewport, and headless mode. Playwright documents pixel variance caused by these environment factors; the same controls are important for Selenium baselines.
Troubleshooting false, blank or stale captures
The screenshot shows the old page after a click
Cause: the click returned before the SPA transition or API render completed. Fix: wait for the old panel or spinner to disappear and for a new marker, text value, or route-specific element to appear.
The file is blank but the command succeeded
Cause: capture occurred before content was inserted, the wrong frame was active, or a blank/error document loaded. Fix: wait for a visible application marker, verify the current URL and frame, and inspect dimensions and pixels instead of trusting the write result.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Only images or icons are missing
Cause: lazy loading, a blocked request, or a font/icon resource still loading. Fix: trigger the required scroll, wait for image completion and nonzero natural dimensions, then check resource failures in browser logs.
Text wraps differently between runs
Cause: fonts, viewport, DPR, operating system, or browser version changed. Fix: wait for fonts, pin the environment, and set the window size and scale factor explicitly.
The capture is clipped or unexpectedly short
Cause: the driver captured the viewport or element rather than the full document, or the implementation’s full-page behavior differs. Fix: verify the supported screenshot surface and use a documented full-page technique for that driver.
Rank #4
Artifacts appear to come from a previous test
Cause: a relative path, parallel workers, or overwrite reused an earlier filename. Fix: print Path.resolve(), include a run identifier, and verify modification time and byte size before comparison.
Recommended Free Tools
Performance and reliability trade-offs
Short polling intervals make readiness detection responsive but can increase command traffic; intervals around a few hundred milliseconds are usually sufficient for UI state checks. Keep timeouts long enough for the slowest supported environment, but fail with a message naming the missing condition. Waiting for every image on a page can waste time when only one component matters, so scope checks to the pixels under test.
For regression suites, separate three failures: readiness timeout, capture/I/O failure, and pixel mismatch. The first indicates an application or selector problem; the second indicates an artifact or driver problem; the third is a visual change that can be reviewed. This classification prevents retries from hiding real defects.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
For a URL-only capture, ScreenshotNeo provides a GET endpoint at https://screenshotneo.com. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
Use the ScreenshotNeo API documentation for authentication and options. A cURL request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo supports PNG, JPEG, WebP and PDF output; full-page capture with lazy images loaded; CSS-selector element capture; dark mode; 12 device presets or any viewport; retina scale; PDF paper size, margins, landscape and page ranges; HTML/CSS to image; custom CSS and JavaScript; clicks before capture; hidden selectors; waits for a selector, delay or network idle; blocking ads, trackers, requests or resource types; custom headers, cookies, user agents and Authorization; timezone and geolocation; transparent backgrounds; resizing; selectable-TTL caching; signed public image links; asynchronous jobs with signed webhooks; bulk capture of up to 100 URLs per call; a usage API; and an OpenAPI specification. Parameter names used by other screenshot APIs also work to ease migration.
An MCP server supplies take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients, so an AI agent can capture without you wiring a WebDriver session. Every feature is available on every plan: 1,000 screenshots per month free without a card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing provides two months free.
Best Value
Sign up for the free ScreenshotNeo plan to get 1,000 screenshots a month with no card.
FAQ
Should a browser upgrade automatically invalidate visual baselines?
Review and regenerate baselines deliberately after an upgrade. First run the old and new browser versions in pinned environments so genuine application changes are not confused with rendering differences.
Free tools Windows power users keep installed
One-click scans. No signup required.
Can retries make a flaky screenshot test reliable?
Retries can hide a race. Capture the readiness diagnostics and fix the missing application condition; use retries only for separately identified infrastructure failures.
Frequently Asked Questions
Should a browser upgrade automatically invalidate visual baselines?
Review and regenerate baselines deliberately after an upgrade. First run the old and new browser versions in pinned environments so genuine application changes are not confused with rendering differences.
Can retries make a flaky screenshot test reliable?
Retries can hide a race. Capture the readiness diagnostics and fix the missing application condition; use retries only for separately identified infrastructure failures.
Quick Recap
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.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.

