Migrate from Loki
Loki renders each Storybook story in headless Chrome (Docker, local, or Lambda — plus iOS/Android simulators) and pixel-diffs the screenshots.
Loki is alive, but pre-1.0 and Storybook-locked
Loki is still maintained (latest v0.35.1, Aug 2025), but it's single-maintainer, pre-1.0, and slow to track Storybook majors — and it's Storybook-only: it can't test full application pages or live URLs, and there's no central review UI (file-based baselines + a local loki approve). If you've outgrown Storybook-only testing, or want hosted review, Dungbeetle is a maintained step up.
Why Dungbeetle
| Loki | Dungbeetle | |
|---|---|---|
| Scope | Storybook stories only | Any URL — stories and full pages, terminal, desktop, API, perf, games |
| Diff | Pixel | Structured tree (+ tolerant pixel fallback) |
| Engine | Chrome (Docker/Lambda/local) + mobile sims | Playwright/Chromium |
| Central review | No (.loki/ PNGs + CLI approve) | Yes — self-host or managed |
| Release cadence | Pre-1.0, slow | Active |
Mobile simulators
Loki can screenshot stories in iOS/Android simulators (React Native). Dungbeetle's web capture is Chromium-based and doesn't drive device simulators — if simulator screenshots are core to your suite, that part won't map. Web/Storybook stories migrate cleanly.
Point Dungbeetle at your stories
Loki discovers stories from Storybook automatically; Dungbeetle captures URLs, so you target each story's static iframe URL. Build Storybook, then list the stories you want as web targets:
npm run build-storybook # outputs storybook-static/
npx http-server storybook-static -p 6006 # or any static serverA story's iframe URL is /iframe.html?id=<story-id> (the id from the Storybook URL's ?path=/story/<id>):
// dungbeetle.config.json
{
"version": 1,
"project": { "name": "design-system" },
"lifecycle": {
"start": ["npx http-server storybook-static -p 6006"],
"wait": { "url": "http://localhost:6006", "timeoutMs": 30000 },
"capture": [
{ "kind": "web", "name": "button--primary",
"driver": "playwright", "screenshot": true,
"url": "http://localhost:6006/iframe.html?id=button--primary",
"viewport": { "width": 1366, "height": 768 } },
{ "kind": "web", "name": "button--disabled",
"driver": "playwright", "screenshot": true,
"url": "http://localhost:6006/iframe.html?id=button--disabled",
"viewport": { "width": 1366, "height": 768 } }
]
}
}Stories also have structure
With driver: "playwright" and screenshot: true you match Loki's pixel behaviour. But the story iframe is also DOM — drop screenshot and Dungbeetle diffs the structured component markup, which is far less flaky than pixel diffs for most components. Use pixels where rendering fidelity matters, structure elsewhere.
Field mapping (Loki config lives under a loki key in package.json):
| Loki | Dungbeetle |
|---|---|
| story (auto-discovered) | one web target per story iframe URL |
configurations[].width / height | capture[].viewport |
configurations[].preset (e.g. "iPhone 7") | capture[].viewport (set width/height) |
configurations[].target (chrome.docker, …) | driver: "playwright" + browser |
chromeSelector | capture the iframe (the story root); use lifecycle.wait for readiness |
diffingEngine / threshold | comparison.pixelTolerance |
.loki/reference/ | dungbeetle.snapshots/ |
Map the workflow
| Loki | Dungbeetle |
|---|---|
loki update | dungbeetle update |
loki test | dungbeetle test (or dungbeetle ci) |
loki approve | re-run dungbeetle update, or promote in the cloud review UI |
dungbeetle update # ≈ loki update → dungbeetle.snapshots/
dungbeetle test # ≈ loki test
dungbeetle ci --json report.json --html report.html # in CICommit dungbeetle.snapshots/; the .loki/reference PNGs don't carry over.
Add hosted review
Loki has no shared review surface. Push to a Dungbeetle cloud server for hosted baselines, a review/approve/promote UI, and flakiness analytics:
dungbeetle push --report report.json \
--server "$DUNGBEETLE_SERVER_URL" \
--client-id "$DUNGBEETLE_CLIENT_ID" --client-secret "$DUNGBEETLE_CLIENT_SECRET"