Visual Regression Testing with WebdriverIO
Set up WebdriverIO visual tests with reviewed baselines, stable captures, and practical fixes for noisy diffs.
To add visual regression testing to WebdriverIO, install @wdio/visual-service, register it in your WDIO configuration, and use its screen, element, or full-page check methods in a test. The first check can create a baseline; on later runs, inspect any difference and update the baseline only when the change is intentional. Visual tests compare rendered appearance, so keep functional assertions and accessibility checks alongside them.
1. Install and configure the visual service
The examples below use a JavaScript WebdriverIO project with Mocha. The service also supports Jasmine and CucumberJS. Follow the official WebdriverIO visual testing guide for version-specific setup and options.
npm install --save-dev @wdio/visual-service
Add the service to your existing wdio.conf.js or wdio.conf.ts. Keep your project’s existing runner, browser, and test-spec settings; this is the visual-service portion of the configuration:
// wdio.conf.js
import path from 'node:path';
export const config = {
// Keep the rest of your WebdriverIO configuration here.
services: [
[
'visual',
{
baselineFolder: path.join(process.cwd(), 'tests', 'baseline'),
formatImageName: '{tag}-{logName}-{width}x{height}',
screenshotPath: path.join(process.cwd(), 'tmp'),
savePerInstance: true,
},
],
],
};
These settings put accepted reference images in tests/baseline, name images with identifying metadata, and keep screenshot output under tmp. The service has additional service-level and method-level options; use the service options reference for the complete current list.
2. Add a visual test
Capture a state that matters to a user: navigate to the page, wait for the relevant content, and then compare. A first-run check can create the baseline automatically when one is missing.
// tests/visual/home.visual.spec.js
describe('Home page visual appearance', () => {
it('matches the home page baseline', async () => {
await browser.url('http://localhost:3000');
await $('[data-testid="home-ready"]').waitForDisplayed();
// Screen-sized capture: useful for the visible viewport and its layout.
await browser.checkScreen('home-page');
});
it('matches the primary navigation component', async () => {
await browser.url('http://localhost:3000');
const navigation = $('[data-testid="primary-navigation"]');
await navigation.waitForDisplayed();
// Element capture limits the comparison to a meaningful component.
await browser.checkElement(navigation, 'primary-navigation');
});
it('matches the full page', async () => {
await browser.url('http://localhost:3000');
await $('[data-testid="home-ready"]').waitForDisplayed();
await browser.checkFullPageScreen('home-full-page');
});
});
Replace the local URL and test IDs with your application’s route and stable readiness signal. The methods above are provided by the service after it is registered. You can also use the service’s visual matchers; see Writing Tests for examples and framework details.
3. Create and review baselines
- Run the visual test in the environment you intend to use as the reference.
- Inspect the generated baseline image to confirm it shows the intended state, at the intended dimensions.
- Commit reviewed baselines so CI and developers compare against the same reference.
- On a later run, inspect the actual image and diff when a check fails. Decide whether the difference is an intended UI change or an unexpected regression.
- Update the baseline only after reviewing and accepting an intentional change. Keep the prior reference when the difference is unexplained.
For the first run, do not combine explicit save and compare calls for the same image. The check methods can create a missing baseline. If automatic baseline saving is disabled, the service reports where the actual image was saved so it can be reviewed and copied into the baseline location deliberately.
4. Choose the right capture scope
| Scope | Use it for | Trade-off |
|---|---|---|
| Screen | A route’s visible viewport, page shell, or above-the-fold layout. | Does not represent content below the viewport. |
| Element | A stable component such as a navigation bar, pricing panel, or form. | Requires a reliable selector and does not catch changes outside that element. |
| Full page | Long pages where section ordering, spacing, and below-the-fold content matter. | More content can mean more sources of variation; lazy or scroll-triggered content may need a different capture mode. |
The visual service documents desktop Chrome, Firefox, Safari, and Microsoft Edge, plus Appium-mediated Android and iOS emulators, simulators, and real devices, including native and hybrid contexts. Actual coverage depends on the browsers, devices, and Appium infrastructure configured in your runner.
5. Keep screenshots stable
A visual test is only useful when unrelated capture differences are controlled. Use the same browser, viewport, fonts, application data, and runtime configuration for baseline creation and comparison.
- Wait for application readiness. A page-load event does not guarantee that your data, animations, or fonts are ready. Wait for a meaningful application selector and, where needed, explicitly wait for fonts or data. WebdriverIO notes that fonts can load asynchronously after it considers the page loaded.
- Use deterministic data. Fix dates, randomized content, rotating banners, and user-specific values where possible. Hide or mask a dynamic region when it is outside the purpose of the test.
- Reduce rendering noise. The service can hide scrollbars, optionally disable blinking input carets, and hide text when the goal is to compare layout rather than glyph rendering. Choose these controls intentionally: hiding text can conceal text-specific regressions.
- Handle lazy loading. Desktop full-page capture uses WebDriver BiDi without scrolling by default. Set
userBasedFullPageScreenshotwhen content appears only after scrolling; this scrolls through the page and stitches viewport captures. ThefullPageScrollTimeoutoption controls the wait after a scroll. Tune it to the page’s behavior rather than assuming one delay fits every application. - Keep dimensions consistent. Browser version, viewport, device scale, and fonts can change wrapping and pixel output. Run baseline and comparison jobs with the same environment.
See the service options documentation for defaults and supported contexts. These recommendations are ways to control known sources of screenshot variation; they do not guarantee that every test will be free of noise.
6. Understand diffs and thresholds
Visual comparisons report differences in rendered images. A mismatch is a signal to investigate, not proof that the UI is wrong. Review the actual image and diff in context before changing the accepted reference.
WebdriverIO’s v10 visual service changed its comparison engine from ResembleJS to Pixelmatch, which uses a perceptual YIQ color model. The reported mismatch percentage can differ from v9 or earlier, so a threshold that made sense on an older major version is not automatically portable. After upgrading, review failures and baselines; the documentation describes updating individual failing baselines with --update-visual-baseline or recreating a baseline folder when intentionally starting over. Avoid a blanket threshold without understanding what it permits your test to ignore.
7. Run visual tests in CI
- Use a stable browser image and install dependencies from the lockfile.
- Run the same WDIO command and browser configuration used to establish the baselines.
- Preserve failure output and actual screenshots as CI artifacts so reviewers can inspect them.
- Fail the job on unexplained visual changes; update and commit references through the normal review process for intended UI work.
- Keep visual assertions focused on high-value routes and components, and retain functional and accessibility coverage for behavior those image comparisons cannot verify.
For teams using other environments, the service also supports Appium contexts when configured appropriately. A hosted visual workflow may be worth evaluating when centralized review or managed cross-browser and device coordination matters. Percy documents a WebdriverIO integration, and Applitools documents checkpoint and baseline review. Compare storage and review workflow, browser/device coverage, CI integration, collaboration, data handling, and current pricing directly; the research for this guide does not establish neutral pricing or feature parity for those services.
8. Common problems and fixes
| Symptom | Likely cause | What to do |
|---|---|---|
| Baseline missing on the first run | No reference image exists yet, or automatic baseline saving is disabled. | Let a check method create the initial baseline, or inspect the reported actual image and add it to the baseline folder. Avoid combining save and compare on the initial run. |
| Diffs change between runs | Fonts, asynchronous data, animation, dates, viewport, or browser environment changed. | Wait for application readiness, use deterministic content, normalize dynamic regions, and align the capture environment. |
| Full-page image omits lazy content | Content is loaded only as the page scrolls. | Enable userBasedFullPageScreenshot and tune fullPageScrollTimeout for the page’s scroll-triggered rendering. |
| Unexpected failures after upgrading to v10 | The comparison engine changed from ResembleJS to Pixelmatch, changing mismatch percentages. | Review diffs and decide whether to update affected baselines. Do not assume the old threshold has the same meaning. |
| Element check captures the wrong region | The selector matches multiple elements, is unstable, or is resolved before the intended state is visible. | Use a unique stable selector and wait for the intended element to be displayed before checking it. |
| Screenshot differs due to a caret or scrollbar | Transient browser rendering appears in the capture. | Use the service’s scrollbar and blinking-cursor controls where appropriate. |
| CI differs from a developer machine | Browser, fonts, viewport, device setup, or runtime data differ. | Make the baseline and CI capture environment consistent; review the actual image before accepting a new reference. |
9. Performance, reliability, and cost
Visual checks add browser capture and image-comparison work to a test run. Screen and element scopes keep comparisons focused; full-page captures cover more content and may take additional time, especially when user-based scrolling and settling waits are needed. Keep the suite centered on important states, and parallelize only within the capacity of your browser or device infrastructure.
Reliability depends on stable application state and repeatable rendering. Baselines are versioned test inputs, so protect them with code review and ensure a failed run retains enough image output to diagnose the difference. A screenshot check does not establish that controls work, content is correct, or a page is accessible; pair it with tests for those concerns.
The WebdriverIO service is an installable development dependency; this guide does not assert a separate service fee. Hosted products have their own plans and terms, which should be checked directly before choosing them.
Or skip the browser setup
If you need screenshots of public pages without running a WebdriverIO browser, ScreenshotNeo offers a website screenshot API and MCP server. One GET request returns an image or PDF. For this API capture, the ScreenshotNeo API key is required; the sample page URL below is adapted to a documentation page. See the ScreenshotNeo API documentation for the request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://webdriver.io/docs/visual-testing/ -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://webdriver.io/docs/visual-testing/",
},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
import { writeFile } from 'node:fs/promises';
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://webdriver.io/docs/visual-testing/',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners, popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. The API also supports screen and element captures, device and viewport settings, PDF, custom CSS and JavaScript, waiting controls, request blocking, caching, async jobs, and bulk capture. ScreenshotNeo is for capturing pages; it does not replace a WDIO baseline comparison workflow.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a visual test replace an end-to-end test?
No. It compares appearance. Keep assertions for behavior, data, and accessibility in the suite.
Can I use the service with CucumberJS?
Yes. The visual service documentation lists Mocha, Jasmine, and CucumberJS support.
Should every small pixel change fail the build?
Decide based on the purpose of the check and review the diff. Threshold behavior can change across major versions, so treat thresholds as configuration that needs review.
Can I compare mobile and native app screens?
The service supports Appium-mediated mobile devices and native or hybrid contexts when the runner and Appium setup provide them.


