Visual Regression Testing in Drupal
Build reliable Drupal visual regression tests with Backstop Generator, BackstopJS, or Cypress, then automate clean screenshot capture with ScreenshotNeo.

Direct answer: For most Drupal sites, start with Backstop Generator and BackstopJS. Backstop Generator reads Drupal site structure and produces BackstopJS profiles, scenarios, and viewport settings. BackstopJS then captures reference and test screenshots and compares them. If your team already drives Drupal through Cypress, add a visual comparison plugin or service to the Cypress workflow instead.
Visual regression testing answers a focused question: did a rendered page change in an unintended way? It does not replace Drupal unit, kernel, functional, browser, or JavaScript tests. Those tests check logic, permissions, data, and behavior; visual checks protect layout, typography, spacing, component states, and responsive presentation.
What visual regression testing checks
A visual test follows four steps:

- Capture an approved reference image.
- Render the same route and state again.
- Compare the new image with the reference.
- Review the difference and decide whether to accept a new baseline.
A difference can be a real theme regression, an intentional design update, or environmental noise. The comparison tool cannot make that decision for you. A human review is part of the baseline workflow.
Good coverage is deliberate. Include the homepage, high-traffic landing pages, navigation, representative content templates, and critical forms or components. Add states such as an open menu, validation error, logged-in toolbar, or dark mode when those states matter. Avoid taking snapshots of every node and every query variation: incidental differences create review noise and make genuine regressions harder to see.
Choose an approach
| Approach | Best fit | What to evaluate |
|---|---|---|
| Backstop Generator + BackstopJS | Drupal teams wanting Drupal-aware setup | Path and content generation, viewport configuration, local workflow, baseline maintenance, rendering consistency |
| Cypress + visual plugin or service | Teams already using Cypress for browser or end-to-end tests | Reuse of login and UI setup, comparison provider, masking, review workflow, browser coverage, CI integration |
| Hosted visual review service | Teams needing shared review and cross-browser infrastructure | Capture model, device coverage, data handling, region masking, CI behavior, vendor terms and current pricing |
Cypress documents integrations including Applitools, Argos, Chromatic, Happo, LambdaTest SmartUI, Percy, Sauce Labs Visual, SmartBear VisualTest, and Wopee.io. They are candidates to evaluate, not interchangeable Drupal modules. Confirm current compatibility and service terms before adopting one. Chromatic’s Cypress documentation, for example, specifies Cypress 13.5.0 or newer.
Set up Backstop Generator for Drupal
1. Select stable pages and states
Write a small inventory before installing anything:
- Homepage and primary landing pages.
- One representative URL for each important content type and view mode.
- Main navigation, footer, search, and critical forms.
- Authenticated and anonymous states where their layouts differ.
- Responsive breakpoints that correspond to your actual theme.
Decide which regions are allowed to vary. A timestamp, rotating promotion, personalized greeting, or third-party embed should be stabilized, stubbed, or narrowly masked. Do not mask an entire page to make a failing test pass.
2. Install and enable the Drupal module
Backstop Generator is installed through Composer and enabled as a Drupal module. The exact package command can change with the module’s current release, so use the installation command shown on its project page and commit the resulting Composer files. After enabling it, configure a profile and the scenario sources you want it to use: the homepage, enabled languages, menu hierarchy, random nodes by content type, or manually defined paths.
# Run from the Drupal project root; use the command shown by the module release you select
composer require drupal/backstop_generator
vendor/bin/drush en backstop_generator -y
The module writes a backstop.json configuration. BackstopJS is a separate dependency and must be installed and initialized in your project workflow.
3. Install BackstopJS and inspect the generated configuration
npm install --save-dev backstopjs
npx backstop init
Generate or export the Drupal-aware profile according to the module documentation, then open backstop.json and check every scenario. Confirm that URLs point to the intended environment, selectors identify the right elements, and viewports represent real layout breakpoints. Generated scenarios are a starting point; remove unimportant pages and add important states manually.
4. Stabilize the rendering environment
Use the same browser version, operating system strategy, viewport dimensions, device scale, fonts, image assets, and test data for reference and comparison captures. Load web fonts before capture. Freeze dates and random values where possible. Stub variable API responses. Ensure the Drupal cache state and deployed CSS/JavaScript are intentional before recording a baseline.
5. Record and compare baselines
npx backstop reference --config=backstop.json
npx backstop test --config=backstop.json
npx backstop approve --config=backstop.json
Use reference only after a person confirms the page is correct. The test command creates current screenshots and a diff report. Approve a new baseline only when the change is intentional and reviewed. Keep baseline files in version control or in the artifact store your team uses so a pull request can show exactly what changed.
BackstopJS configuration that matters
BackstopJS configuration names vary by release, but these concepts are the ones to review in every scenario:
- URL and label: make each scenario traceable to a route and state.
- Viewport: use a compact set tied to theme breakpoints. Add a desktop, tablet, and mobile width only when each catches a meaningful layout transition.
- Selectors: capture a component or region when full-page noise would hide a useful signal.
- Full-page capture: use it for pages where content below the fold and lazy-loaded images matter.
- Delay and readiness: wait for a selector, network idle, or a short delay only when the page needs it. Excessive fixed delays slow CI and can still be flaky.
- Masking: mask only unavoidable dynamic regions such as an ad slot or live clock.
- Thresholds: keep comparison tolerance tight enough to detect layout changes. Increasing it broadly is not a substitute for stabilizing the page.
- Interaction: click or hover before capture when the important design is behind a menu, dialog, or tab.
Using Cypress for Drupal visual tests
Cypress can drive the browser into a meaningful state, but Cypress’s own screenshot command does not perform image comparison. A plugin or hosted service supplies the comparison and review workflow.
describe('Drupal landing page', () => {
it('matches the approved visual state', () => {
cy.visit('/campaign/spring');
cy.get('[data-testid="hero"]').should('be.visible');
cy.get('[data-testid="main-nav"]').click();
cy.get('[data-testid="mobile-menu"]').should('be.visible');
// Replace this call with the visual plugin or service used by your team.
cy.visualSnapshot('campaign-spring-mobile-menu');
});
});
Prefer stable data-testid attributes or semantic selectors over brittle generated class names. Use element-level snapshots for components whose surrounding content changes often. Keep the setup that creates the state in Cypress: log in, select a language, submit a form, or open a dialog before the visual checkpoint.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One request captures a PNG, JPEG, WebP, or PDF. Its cleaning steps accept cookie and consent banners like a visitor, then remove more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status.
See the ScreenshotNeo API documentation for all options. A minimal capture is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await Bun.write('shot.webp', bytes);
For regression fixtures, use the same URL, viewport, device preset, timezone, geolocation, headers, cookies, user agent, and custom CSS on every run. ScreenshotNeo supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets or any viewport, retina scale, custom JavaScript, clicks, selector or network-idle waits, request and resource blocking, transparent backgrounds, resizing, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. It also accepts parameter names used by other screenshot APIs, which can simplify migration.
An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can collect visual evidence during a review.
Try ScreenshotNeo: cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; 1,000 screenshots a month are free with no card and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Make Drupal pages deterministic
- Seed fixture content and reference it by stable paths.
- Disable or freeze rotating hero content, dates, counters, and random recommendations.
- Use local or pinned font and image assets where licensing and deployment allow.
- Wait for the main content and critical images, not an arbitrary long sleep.
- Stub external APIs and third-party embeds.
- Run captures against a known build, with migrations and configuration imports complete.
- Keep masks small and document why each exists.
Containerized browser execution can complicate GUI access. The Drupal Automated Testing Kit documentation recommends installing Cypress or Playwright on the host while running Drupal in environments such as DDEV, Lando, or Docksal. Check the project’s current maintenance and security status before adopting it.

Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Every screenshot differs by text position | Font missing, late, or different browser rendering | Pin the browser and fonts; wait for font loading; use the same capture environment. |
| Only images differ | Lazy loading, unstable CDN asset, or animation | Wait for the image selector, disable animation, and use stable fixture assets. |
| Large diff around a banner | Consent, newsletter, or chat widget | Remove or block it, or mask only its known region. ScreenshotNeo can remove known consent platforms and widgets before capture. |
| Intermittent blank page | Capture started before Drupal or an API finished loading | Wait for a meaningful selector or network idle; inspect server and browser logs. |
| Logged-in page redirects to login | Session cookie was not supplied or expired | Create the session in the browser workflow or pass the required cookie and headers securely. |
| Mobile menu is missing | Wrong viewport or no interaction before capture | Use the theme breakpoint and click the menu trigger before taking the snapshot. |
| CI fails but local passes | Different browser, fonts, timezone, data, or viewport | Compare environment versions and make them explicit; do not immediately raise the diff threshold. |
| Backstop command cannot find configuration | BackstopJS was not initialized or path is wrong | Run from the project root, initialize BackstopJS, and pass the correct config path. |
Performance, reliability, and cost
Each additional route, viewport, and state multiplies capture time. Begin with a small representative matrix, run it on every theme or component change, and schedule broader content coverage separately. Element snapshots are usually faster and easier to review than full-page captures, while full-page checks are valuable for long landing pages and lazy-loaded content.
Cache immutable assets and use a controlled browser pool in CI. Parallelize independent scenarios only when the runner has enough CPU and memory; excessive concurrency can cause timeouts and make rendering less consistent. Retry transient navigation failures once with diagnostics, but do not hide repeatable failures with unlimited retries.
For ScreenshotNeo, cache hits are not billed, and failed loads, timeouts, blank pages, and bot checks are not billed. Its plans include Free with 1,000 shots per month and no card, Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan. Use its usage API and X-Page-Verdict/X-Billed response headers to reconcile CI activity.
Baseline review checklist
- Was the diff produced from the intended Drupal build and content fixture?
- Are browser, fonts, viewport, scale, timezone, and network conditions consistent?
- Does the changed region correspond to the code or configuration in the pull request?
- Is the change visible at more than one relevant breakpoint?
- Could a missing asset, failed request, or consent widget explain it?
- Has a person approved the change before the baseline is updated?
FAQ
Does Drupal include visual regression testing?
Drupal provides several automated testing layers, but screenshot comparison is normally added with BackstopJS, Cypress plus a visual provider, or another dedicated tool.
Should every Drupal page be captured?
No. Choose representative templates, high-value routes, shared components, and important interaction states. Broadly capturing incidental pages increases maintenance without proportional coverage.
Can I compare only one Drupal component?
Yes. BackstopJS and Cypress workflows can target an element or region. Component-level checks reduce unrelated diff noise.
When should a baseline change be approved?
Only after reviewing the diff in the context of the code, content, and environment change and deciding that the new rendering is intentional.
Can an AI agent run these checks?
Yes. ScreenshotNeo’s MCP server exposes screenshot, page-information, and PDF tools to MCP clients such as Claude and Cursor. The agent still needs a deterministic URL and a human decision about whether a visual difference is acceptable.


