絵図 · ezu, an old Japanese picture map
A map of every screen
in your app.
Ezu runs your app, walks the flows you describe in a real browser, and draws every page and UI state from screenshots. Approve the screens once, and ezu check fails when one of them changes.
Two repos, one wrapper
Ezu never goes in your app's Gemfile. The tool lives in its own checkout, and your app keeps a description of itself in an ezu/ folder. A three-line bin/ezu joins them.
your-app/
bin/ezucalls the tool with--apppointing hereezu/config.rbhow to boot the app, locales, the clock, routes to skipezu/flows/*.rbwhat a user clicks through, step by stepezu/seed.rbtest data, run inside the appezu/approved.jsonfingerprints of approved screens, committedtmp/ezu/the map: openindex.html
ezu/ (this tool, beside your app)
exe/ezumap, check, approveadapters/boot a Rails app, or anything started by a commandwalk.rbPlaywright and Chromium, pinnedmodel.rbroutes and walks become pages, states, flowsviewer/one static HTML page that reads the map
What a run does
- Prepare. The adapter makes a throwaway copy of your app: a fresh database, built assets. Your own data is never touched.
- Ask the app. It reads every GET route and every translation, so Ezu knows which pages exist and what the buttons say in each language.
- Start slots. One slot per screen size per language, in parallel. Each has its own database copy, server, port, frozen clock and browser.
- Walk the flows. Each step clicks, waits for what it expects, and takes a light and a dark screenshot once two in a row are identical.
- Draw the map. Steps become states on pages, compared with the approved screenshots or the previous run, and written to
tmp/ezu/.
A flow reads like a user
Each step names the state it lands on and how the user got there. Flows find things by the app's own translation keys, so the same flow runs in every language.
flow "sign-up", title: "Sign up", area: "Account",
summary: "A visitor creates an account." do |walk|
walk.step("Landing page", via: "Open the app", main: true) do |page|
walk.visit("/")
walk.see(walk.t("home.headline"))
end
walk.step("Sign-up form", via: "Get started", main: true) do |page|
page.get_by_role("link", name: walk.t("home.cta")).click
page.locator("#user_email").wait_for(state: "visible")
end
end
Three commands
Map to look, approve what is right, check in CI. Approvals are SHA-256 fingerprints, so the repo never grows by a screenshot.
bin/ezu map # walk every flow, write tmp/ezu/
open tmp/ezu/index.html
bin/ezu approve --all # after reviewing the first map
bin/ezu check # exit 1 when a screen moved
Map your own app
A Rails app needs a config, one flow and a wrapper. This site is mapped the same way: its docs/ezu/ folder is the smallest working example.