How to Capture Screenshots in Automated Testing
Learn how to capture reliable screenshots in Playwright, Cypress, and Selenium, debug failures, and compare visual baselines in CI.
Use your browser automation framework’s screenshot API after the application reaches the state you want to verify. Use Playwright’s toHaveScreenshot() for visual comparisons, Cypress’s cy.screenshot() for image artifacts, or Selenium’s screenshot methods for a current page or element. Save failure images as CI artifacts, and keep the rendering environment and test data stable before comparing pixels.
1. Choose the screenshot goal first
| Goal | Capture | Best practice |
|---|---|---|
| Failure debugging | The page when an assertion fails | Attach the image, video, console log, and trace to the CI job. |
| Component regression | A single element | Compare the smallest useful region to reduce unrelated diffs. |
| Viewport regression | The visible viewport | Pin viewport size, browser, fonts, and device scale. |
| Page-layout regression | The full page | Check sticky headers, lazy content, and long-page stitching. |
| Documentation or evidence | A named PNG, JPEG, or WebP artifact | Use deterministic data and store it with the test run. |
A physical screen capture device is not part of the normal browser automation workflow. The browser driver renders the page and returns image bytes.
2. Playwright: capture and compare screenshots
Playwright Test includes screenshot assertions. The first run creates a reference image; later runs wait for two consecutive matching screenshots and compare the result. PNG is the default; a filename ending in .webp selects WebP. See the Playwright visual comparisons documentation.
Install and run
npm init playwright@latest
npx playwright test
Page and element comparisons
import { test, expect } from '@playwright/test';
test('stable page visual state', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.getByRole('heading', { name: 'Example Domain' })).toBeVisible();
await expect(page).toHaveScreenshot('example-page.png', {
animations: 'disabled',
caret: 'hide'
});
});
test('component visual state', async ({ page }) => {
await page.goto('https://example.com');
const card = page.locator('.pricing-card').first();
await expect(card).toBeVisible();
await expect(card).toHaveScreenshot('pricing-card.png');
});
Use options such as clip for a rectangle, mask for dynamic elements, and diff thresholds when a small rendering difference is acceptable. Keep thresholds narrow: a broad tolerance can hide a real regression.
Update a reviewed baseline
npx playwright test --update-snapshots
Review the changed files in the pull request. Playwright warns that pixels can vary with the host OS, browser version, settings, hardware, power source, and headless mode, so generate and compare snapshots in the same environment.
3. Cypress: save screenshots and failure evidence
Use cy.screenshot() for an explicit image. During cypress run, Cypress also captures a screenshot when a test fails; this automatic behavior does not occur in cypress open. The default folder is cypress/screenshots. See the Cypress screenshot command and Cypress visual testing guidance.
describe('checkout', () => {
it('captures the completed state', () => {
cy.visit('/checkout');
cy.intercept('GET', '/api/cart', { fixture: 'cart.json' }).as('cart');
cy.wait('@cart');
cy.get('[data-testid="checkout-form"]').should('be.visible');
cy.screenshot('checkout-ready');
});
});
Configure failure captures
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
screenshotOnRunFailure: true,
screenshotsFolder: 'cypress/screenshots',
e2e: { baseUrl: 'http://localhost:3000' }
});
Cypress supports viewport, full-page, and runner captures. Full-page mode scrolls and stitches the application; fixed or sticky elements can appear more than once. Cypress captures images but does not itself compare them. Use a visual-comparison plugin or service, keep baselines under review, and mask only genuinely dynamic regions.
4. Selenium WebDriver: save the current page or an element
Selenium bindings expose equivalent operations with language-specific names. In Python, save_screenshot writes a PNG. Drivers may also return Base64 data, and full-page behavior differs by browser and binding. Check the Selenium screenshot documentation.
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
options = webdriver.ChromeOptions()
options.add_argument('--headless=new')
options.add_argument('--window-size=1440,900')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://example.com')
heading = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.TAG_NAME, 'h1'))
)
driver.save_screenshot('example-page.png')
heading.screenshot('example-heading.png')
finally:
driver.quit()
Java uses TakesScreenshot; JavaScript bindings use takeScreenshot(). Confirm whether your binding returns a file, bytes, or Base64 before writing it to disk.
5. Make captures deterministic
- Wait for an assertion about application state, not only a fixed sleep.
- Stub APIs or load a fixture so prices, timestamps, text, and result ordering do not change.
- Disable animations, transitions, caret blinking, videos, and rotating content.
- Pin browser version, operating system, fonts, viewport, device scale factor, and headless settings.
- Use a targeted element screenshot for component changes; use full-page comparisons for page-level layout.
- Mask or hide only known dynamic regions. Overly broad masks can conceal regressions.
- Load lazy images before a full-page capture and wait for fonts and critical network requests.
// Playwright example: freeze common motion before capture
await page.addStyleTag({ content: `
*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
` });
6. Run screenshots in CI and review changes
- Start the application with a fixed configuration.
- Run tests in a pinned browser image or container.
- Write screenshots, diffs, traces, and logs to predictable directories.
- Upload failure images as CI artifacts even when they are not baselines.
- For visual tests, require a human review of baseline changes.
- Regenerate baselines only when the design change is intentional and the test data is unchanged.
Local comparison keeps images in your infrastructure. Hosted visual-testing services can add browser coverage, dashboards, and approval workflows, but review their data-handling and pricing terms before sending artifacts.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is blank | Capture ran before navigation or rendering completed. | Wait for a meaningful locator, network condition, or application-ready signal. |
| Intermittent pixel diffs | Animations, fonts, timestamps, ads, or changing API data. | Freeze motion, preload fonts, stub data, and mask only the dynamic node. |
| Full-page image repeats a header | Sticky or fixed positioning during Cypress stitching. | Use a viewport or element capture, or temporarily disable the sticky rule. |
| Element screenshot fails | Element is hidden, detached, outside the frame, or covered. | Wait for visibility, scroll it into view, and assert its stable state. |
| Baseline differs only in CI | Different browser, OS, fonts, device scale, or headless mode. | Use the same container and browser build for generation and comparison. |
| Images are too large or slow | Full-page captures at a large viewport or high scale. | Capture the smallest scope, reduce scale, and retain full-page images only where needed. |
| Cypress has no failure image | Test ran interactively or failure capture was disabled. | Run with cypress run and set screenshotOnRunFailure: true. |
| Selenium image format is wrong | Binding returned Base64 or bytes instead of a path. | Decode or write the returned data according to that binding’s API. |
8. Performance, reliability, and cost
- Performance: element and viewport captures are usually cheaper than scrolling and stitching a long page. Avoid taking screenshots after every step unless the images are needed for diagnosis.
- Reliability: state-based waits and deterministic fixtures matter more than increasing timeout values. A longer arbitrary delay can still capture the wrong state.
- Storage: retain failure artifacts for a useful debugging window; retain approved baselines in version control or your visual-testing system.
- Comparison cost: screenshot capture and pixel comparison are separate capabilities. Cypress’s command creates an image; another tool must compare it.
- Review cost: every baseline update should be reviewed. Automatic approval can turn an application bug into a new baseline.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One request returns a PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP tools let Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
Use the ScreenshotNeo documentation for all options, including full-page or CSS-selector captures, device presets, dark mode, retina scale, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching TTLs, signed links, asynchronous webhooks, bulk capture, usage data, and the OpenAPI specification.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 shots a month with no card. Starter is $5 for 3,000 shots; yearly billing gives two months free. Paid plans start at $5, and every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Should I compare a whole page or an element?
Compare an element when the change belongs to a component. Use a whole page when navigation, layout, or responsive composition is the subject.
Why do screenshots differ on two machines?
Browser rendering depends on the OS, browser build, fonts, hardware, device scale, and headless mode. Pin those inputs or compare inside one CI image.
Does Cypress compare screenshots automatically?
No. cy.screenshot() creates an image. A plugin or service must perform visual comparison and baseline review.
Can Selenium take a full-page screenshot everywhere?
Do not assume it can. Full-page support varies by browser, driver, and binding; verify the behavior for your target environment.
When should I update a baseline?
Only after confirming that the visual change is intentional, the test state is deterministic, and the diff contains no unrelated changes.


