Migrate from BackstopJS
BackstopJS captures URL/element screenshots with Puppeteer (or Playwright), diffs them with Resemble.js, and shows an interactive HTML report. It's still widely used — but drifting.
BackstopJS maintenance is stalling
There's been no new npm release in roughly a year and Snyk classifies the package's maintenance as inactive. It isn't archived, but dependency and security drift is the trajectory — and it has no central, hosted review UI (baselines are git-committed PNGs, approvals are a local CLI). A good time to move to a maintained tool with real review.
Why Dungbeetle
| BackstopJS | Dungbeetle | |
|---|---|---|
| Maintenance | Inactive (no release ~12 mo) | Active |
| Diff | Pixel (Resemble.js) | Structured tree (+ tolerant pixel fallback) |
| Engine | Puppeteer / Playwright | Playwright/Chromium |
| Approve flow | backstop approve (local CLI) | local re-baseline or hosted promote in the review UI |
| Central review | No (local HTML report) | Yes — self-host or managed |
| Agent access | None | MCP server — an agent can triage runs, but promoting a baseline is human-owned approval |
| Cross-machine flakiness | High without the Docker image | Lower — structured diffs don't depend on render pixels |
Translate your config
BackstopJS uses backstop.json. scenarios × viewports become web targets:
// backstop.json
{
"id": "my_project",
"viewports": [
{ "label": "phone", "width": 320, "height": 480 },
{ "label": "desktop", "width": 1920, "height": 1080 }
],
"scenarios": [
{
"label": "Homepage",
"url": "https://example.com",
"selectors": ["document"],
"delay": 500,
"misMatchThreshold": 0.1
}
],
"engine": "puppeteer"
}// dungbeetle.config.json
{
"version": 1,
"project": { "name": "my_project" },
"lifecycle": {
"capture": [
{ "kind": "web", "name": "Homepage-phone", "driver": "playwright", "url": "https://example.com", "screenshot": true, "viewport": { "width": 320, "height": 480 } },
{ "kind": "web", "name": "Homepage-desktop", "driver": "playwright", "url": "https://example.com", "screenshot": true, "viewport": { "width": 1920, "height": 1080 } }
]
},
"comparison": { "pixelTolerance": { "maxChangedRatio": 0.001 } }
}Field mapping:
| BackstopJS | Dungbeetle |
|---|---|
scenarios[].url | capture[].url |
scenarios[].label × viewports[].label | capture[].name (one target per scenario × viewport) |
viewports[] | capture[].viewport |
misMatchThreshold (%) | comparison.pixelTolerance.maxChangedRatio (ratio: 0.1% → 0.001) |
engine (puppeteer/playwright) | driver: "playwright" + browser.channel |
delay / readySelector | lifecycle.wait (e.g. wait on a URL/selector before capture) |
selectors / hideSelectors | see below |
paths.bitmaps_reference | dungbeetle.snapshots/ (baselines dir) |
hideSelectors → masking, or just use the DOM diff
BackstopJS hides dynamic regions (hideSelectors) to stop pixel flakiness. In Dungbeetle, two better options: (1) capture the structured DOM (drop screenshot) so a single changed node reads as one node change instead of a repaint; (2) for dynamic text (timestamps, IDs), add a normalization mask that replaces the value everywhere before comparison. You usually need far fewer "ignore" rules than with pixels.
Map the workflow
| BackstopJS | Dungbeetle |
|---|---|
backstop reference | dungbeetle update |
backstop test | dungbeetle test (or dungbeetle ci for JSON/HTML) |
backstop approve | re-run dungbeetle update, or promote in the cloud review UI |
dungbeetle update # ≈ backstop reference
dungbeetle test # ≈ backstop test
dungbeetle ci --json report.json --html report.html # in CI (replaces JUnit + HTML report)Commit dungbeetle.snapshots/. Your backstop_data/bitmaps_reference PNGs don't carry over — re-baseline once.
Add central review (what BackstopJS lacks)
BackstopJS approvals are local. Push to a Dungbeetle cloud server for a shared, audited review → approve → promote flow across machines and repos:
dungbeetle push --report report.json \
--server "$DUNGBEETLE_SERVER_URL" \
--client-id "$DUNGBEETLE_CLIENT_ID" --client-secret "$DUNGBEETLE_CLIENT_SECRET"