Skip to content

絵図 · 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 --app pointing here
  • ezu/config.rbhow to boot the app, locales, the clock, routes to skip
  • ezu/flows/*.rbwhat a user clicks through, step by step
  • ezu/seed.rbtest data, run inside the app
  • ezu/approved.jsonfingerprints of approved screens, committed
  • tmp/ezu/the map: open index.html

ezu/ (this tool, beside your app)

  • exe/ezumap, check, approve
  • adapters/boot a Rails app, or anything started by a command
  • walk.rbPlaywright and Chromium, pinned
  • model.rbroutes and walks become pages, states, flows
  • viewer/one static HTML page that reads the map

What a run does

  1. Prepare. The adapter makes a throwaway copy of your app: a fresh database, built assets. Your own data is never touched.
  2. 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.
  3. Start slots. One slot per screen size per language, in parallel. Each has its own database copy, server, port, frozen clock and browser.
  4. 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.
  5. 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.

Writing flows →

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.

Approve and check →

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.