Skip to content
Pages

Add Ezu to an app

This page sets up Ezu for a Rails app, from nothing to an approved map. An app that isn't Rails uses the command adapter and the same steps.

After reading this page, you will know:

  • What to install, and where.
  • Which files your app needs, and what goes in each.
  • How to run the first map, approve it and check it.

Requirements

  • Ruby 3.3 or later, and Node with npm.
  • A Rails app that can load its schema into SQLite. Ezu points DATABASE_URL at a throwaway SQLite file, so the app's own databases are never touched.
  • A health route that answers 200 once the app is up, /up by default.

Install the tool beside your app

Clone Ezu next to your app, so ../ezu from your app is the tool:

cd ~/workspace
git clone git@github.com:davafons/ezu.git
cd ezu
bundle install
npm install
npx playwright install chromium   # once per machine, if Chromium isn't there yet

Playwright and Chromium are pinned. A new Chromium can move pixels, so upgrading it means approving every screen again.

Add the wrapper

bin/ezu in your app runs the tool with its own Gemfile and points it at your app. The tool's path is ../ezu unless EZU_PATH says otherwise:

#!/usr/bin/env bash
set -euo pipefail
app="$(cd "$(dirname "$0")/.." && pwd)"
tool="${EZU_PATH:-$app/../ezu}"
BUNDLE_GEMFILE="$tool/Gemfile" exec bundle exec "$tool/exe/ezu" "$@" --app "$app"

Make it executable with chmod +x bin/ezu, and add /tmp/ezu* to .gitignore if tmp/ isn't ignored already.

Describe the app

ezu/config.rb tells Ezu how to boot the app and what it looks like. The smallest one is a single line:

adapter :rails

A real app usually says more:

adapter :rails,
  prepare: %w[db:schema:load tailwindcss:build],
  env: { "LAUNCHY_DRY_RUN" => "true" }   # no browser windows from letter_opener
locales "en", "ja"
timezone "Asia/Tokyo"
moment Time.utc(2026, 11, 10, 1, 0)     # every run happens at this moment
seed "seed.rb"

cookies do |locale:, seed:, signed_in:|
  [ { name: "locale", value: locale },
    ({ name: "session_id", value: seed.fetch(:session_cookie) } if signed_in) ].compact
end

exclude %r{\A(api|admin|rails)(/|#)}
area "Public site", /\A(home|pages)#/
area "Account", /\A(sessions|accounts)#/

Every setting is in Configuration.

Seed test data

If flows need data that already exists, such as a signed-in user, ezu/seed.rb creates it. It runs through bin/rails runner once per slot, against that slot's database, and writes JSON keyed by slot name to $EZU_SEED_OUT. Whatever it writes reaches the flows as walk.seed and the cookies block as seed:.

require "json"

slot = ENV.fetch("EZU_SLOTS")         # such as "desktop-en"
I18n.locale = ENV.fetch("EZU_LOCALE")

user = User.create!(email_address: "mika-#{slot}@example.com", name: "Mika")
session = user.sessions.create!
cookie = ActionDispatch::TestRequest.create.cookie_jar
  .tap { it.signed[:session_id] = session.id }[:session_id]

File.write(ENV.fetch("EZU_SEED_OUT"), JSON.generate(slot => { session_cookie: cookie }))

Write the first flow

Flows live in ezu/flows/ and load in file name order. Start with one step per page you care about:

# ezu/flows/1-public.rb
flow "public", title: "Browse the public site", area: "Public site",
  summary: "A visitor reads the landing page and the pricing page." 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("Pricing", via: "Pricing in the header", main: true) do |page|
    page.get_by_role("link", name: walk.t("nav.pricing")).click
    walk.see(walk.t("pricing.title"))
  end
end

Each page needs exactly one step marked main: true. Writing flows covers the rest.

Run the first map

bin/ezu map
open tmp/ezu/index.html

The first run prints what it does: preparing the throwaway copy, walking each slot, then a summary of pages, states and screenshots. If a step fails, the run stops, leaves the previous map in place, and prints the step plus the path of a screenshot of where it got stuck.

--locales en runs only some of the app's languages, for a quicker loop while writing flows.

Approve it, then check it

Look through the map. When the screens are right:

bin/ezu approve --all
git add ezu/approved.json

From now on, bin/ezu check maps again and exits with 1 if any screen moved. Run it before you push, or as a step in CI. Approve and check explains reviewing differences.

The example in this repo

This documentation site is a small Rails app, and it is mapped the same way. Read docs/bin/ezu, docs/ezu/config.rb and docs/ezu/flows/ in the Ezu repo for a working setup with no database and no seed. The example map is its output.