@hmp-global/ddev-visdiff
v0.1.7
Published
DDEV-focused visual regression checks for comparing a local site against production.
Readme
ddev-visdiff
A lightweight visual regression CLI that compares a local DDEV site against a live site.
It is designed for DDEV-based local development:
- live screenshots come from
liveBaseUrl - local screenshots come from the DDEV network by default
- each site owns its own important page list
- optional preparation commands can refresh the local database from production first
- reports are written as static HTML under
.visdiff/reports/ - Chromium runs inside a dedicated DDEV service based on the Playwright image
- an optional
pre-pushhook runsddev visdiffbefore code is pushed
Setup
Install the add-on from the DDEV project root:
ddev add-on get HMP-Global/ddev-visdiff
ddev restartThe add-on creates:
.ddev/docker-compose.visdiff.yaml.ddev/visdiff/.ddev/visdiff/Dockerfile.ddev/commands/host/visdiff
The visdiff service builds from mcr.microsoft.com/playwright:v1.60.0-noble, so Chromium is part of DDEV instead of the host machine. The image also installs the Platform.sh CLI and MySQL/PostgreSQL clients for preparation commands. The CLI runtime is vendored under .ddev/visdiff/, isolated from the application dependencies.
From the site repository root, create a config:
ddev visdiff init --live https://example.comRun manually:
ddev visdiff runRun manually and refresh production content first:
ddev visdiff run --prepareInstall the Git hook from the same site repository:
ddev visdiff install-hook --ddevVerify the tool from this package checkout:
npm run check
npm run smokenpm Fallback
The package is also published to npm for direct CLI development and manual installs:
npm install --save-dev @hmp-global/ddev-visdiff
npx ddev-visdiff install-ddevThe DDEV add-on install is the preferred path for project use.
Config
init creates .visdiff.json in the current repository.
{
"liveBaseUrl": "https://example.com",
"localBaseUrl": "auto:ddev",
"checks": [
{ "name": "Home", "path": "/" },
{ "name": "About", "path": "/about" },
{ "name": "Contact", "path": "/contact" }
],
"viewports": [
{ "name": "desktop", "width": 1440, "height": 1000 },
{ "name": "mobile", "width": 390, "height": 844 }
],
"databaseDump": {
"enabled": false,
"runBeforeCompare": false,
"environment": "production",
"file": ".visdiff/latest.sql",
"import": true,
"importShell": "mysql --host=db --user=db --password=db db < {file}"
},
"stageFileProxy": {
"enabled": false,
"origin": "",
"originDir": "",
"drush": "./vendor/bin/drush",
"cacheRebuild": true,
"install": true,
"docroot": "web",
"localModulePath": ".visdiff/modules/stage_file_proxy",
"repository": "https://git.drupalcode.org/project/stage_file_proxy.git",
"ref": "3.1.6",
"guardGit": true
},
"prepareCommands": [],
"threshold": 0.01,
"waitUntil": "networkidle",
"fullPage": true,
"waitForMedia": true,
"mediaTimeoutMs": 10000,
"freezeMedia": true,
"failOnMediaError": true,
"hideSelectors": [],
"ignoreSelectors": [],
"headers": {},
"liveHeaders": {},
"localHeaders": {}
}checks can use different live and local paths when a local route differs:
{
"checks": [
{
"name": "Landing page",
"path": "/",
"livePath": "/campaign",
"localPath": "/campaign?cache-bust=1"
}
]
}DDEV sets auto:ddev to the project's primary HTTPS URL, such as https://example.ddev.site, inside the visdiff service. If DDEV does not expose a primary URL, visdiff falls back to http://web. Use localBaseUrl when you want to override that:
{
"localBaseUrl": "https://example.ddev.site"
}Use localHeaders for ordinary request headers. Chromium does not allow overriding the Host header.
{
"localBaseUrl": "https://example.ddev.site",
"localHeaders": {
"X-Visdiff": "local"
}
}Browser Rendering
The DDEV add-on uses Playwright Firefox by default because the bundled Chromium browser does not decode some MP4/H.264 video backgrounds on Linux/arm64. You can override the browser per project:
{
"browser": "firefox"
}Supported values are chromium, firefox, and webkit. By default, visible video elements must load a frame before screenshots are captured. This prevents a page with a blank hero video from passing only because both live and local failed to render the same media.
Full-page screenshots scroll through the page before capture so lazy-loaded, animated, and parallax sections can initialize. The renderer also stabilizes common scroll animation markup, such as AOS, so below-the-fold sections are visible in the final screenshot. If a project needs true viewport-by-viewport stitching, enable it explicitly:
{
"stitchFullPage": true,
"scrollDelayMs": 250,
"hideRepeatedFixedElements": true
}Production Content Refresh
Preparation commands run inside the DDEV visdiff service.
For most Platform.sh Drupal sites, set the environment name per site:
{
"databaseDump": {
"enabled": true,
"runBeforeCompare": true,
"environment": "production",
"file": ".visdiff/latest.sql",
"import": true
}
}That runs platform db:dump -e production -y --file .visdiff/latest.sql, then imports that dump into DDEV's default database with:
mysql --host=db --user=db --password=db db < .visdiff/latest.sqlFor sites where the main production environment has another name:
{
"databaseDump": {
"enabled": true,
"runBeforeCompare": true,
"environment": "main"
}
}If you only want to create the dump file and import it manually, disable import:
{
"databaseDump": {
"enabled": true,
"environment": "production",
"import": false
}
}For Drupal file assets, use the Stage File Proxy module instead of downloading all production files. Configure stageFileProxy so prepare installs a local-only module copy, enables it in the local database after importing, and points it at production:
{
"stageFileProxy": {
"enabled": true,
"origin": "https://example.com"
}
}When stageFileProxy.enabled is true, prepare clones stage_file_proxy release 3.1.6 into .visdiff/modules/stage_file_proxy, symlinks it into web/modules/contrib/stage_file_proxy, and adds both paths to .git/info/exclude. It then runs ./vendor/bin/drush pm:enable stage_file_proxy -y, configures stage_file_proxy.settings origin, and rebuilds Drupal caches. If your Drush command or docroot lives somewhere else, set stageFileProxy.drush or stageFileProxy.docroot.
The module is intentionally local-only. With stageFileProxy.guardGit enabled, visdiff refuses to run if staged or unpushed Git changes contain Stage File Proxy code, composer.json/composer.lock additions, or exported Drupal config such as stage_file_proxy.settings.yml or core.extension.yml entries.
The Platform CLI needs non-interactive auth inside the container, usually through PLATFORMSH_CLI_TOKEN.
Use ddev visdiff prepare when you want to refresh content without taking screenshots.
Add generated artifacts to the site repository's .gitignore:
.visdiff/reports/
.visdiff/latest.sqlHook behavior
The DDEV pre-push hook runs ddev visdiff run --config .visdiff.json.
When databaseDump.runBeforeCompare is true, the hook refreshes content before screenshots. When visual differences exceed the threshold, the command exits with status 1, prints the report path, and blocks the push.
Ignoring Unstable Areas
Use hideSelectors to hide volatile elements before capture:
{
"hideSelectors": [".cookie-banner", "[data-testid='timestamp']"]
}Use ignoreSelectors to mask areas in both screenshots before comparison:
{
"ignoreSelectors": [".ad-slot", ".map"]
}