Architecture
Ezu is a Ruby gem with an executable, about 1,900 lines including the viewer. Its core knows nothing about any web framework; adapters do.
After reading this page, you will know how a run flows through the code and what each file does.
The pieces
exe/ezu → cli.rb map | check | approve
project.rb loads ezu/config.rb and ezu/flows/*.rb (the DSL)
adapters/rails.rb boots a Rails app; rails/ has the runner scripts
rails/repeatable.rb preloaded into the app: frozen clock, seeded randomness
adapters/command.rb the same interface through shell commands
clock.rb the stepped clock, written to a file the app reads
recorder.rb one Playwright and Chromium per slot; runs every flow
walk.rb one flow in one slot: steps, waits, screenshots
model.rb routes + walks → pages, states, flows; the rules
baseline.rb the approved fingerprints, or the previous run
comparison.rb new / changed / same / gone, pixel diffs in Chromium
viewer/index.html static; reads ezu-data.js and nothing else
A run, in order
cli.rbloads the project and makes one adapter for preparing plus one per slot. A slot is a screen size in a locale; ports are the base port plus the slot's place in the full list.- The preparing adapter makes the throwaway copy and reads routes and translations.
- One thread per slot copies the prepared app, runs the seed, starts the server, and hands a
Recorderits URL. - The recorder opens a browser context per flow, with the slot's viewport, locale, time zone, clock, cookies, blocked URLs and seeded
Math.random, then calls the flow with aWalk. - Each
walk.stepticks the clock, runs the block, waits for the page to settle, fails on bad responses, and captures the screenshots. - When every slot is done,
Modelmatches each step's URL to a route, groups steps into states on pages, and enforces the rules: one main state per page, no flow on an unreachable page. Comparisoncompares with the approved baseline, or the previous run, and writes diffs for changed screenshots.cli.rbwritesezu.json,ezu-data.jsand the viewer totmp/ezu.next/, then swaps it intotmp/ezu/. A failed run never replaces the last good map.
The output
tmp/ezu/
index.html the viewer
ezu-data.js window.EZU = { … }, the same data as ezu.json
ezu.json format 2: pages, states, flows, counts, comparison
screenshots/ <flow>-<step>-<slot>.png, plus -dark.png
previous/ changes/ the baseline's images and pink diffs, for changed states
The viewer depends on ezu.json's shape and nothing else, so another reader, such as a Rails app that keeps history across runs, could use the same file.
Tech stack
| Part | What |
|---|---|
| Language | Ruby 3.3 or later (developed on 4.0) |
| Browser | playwright-ruby-client 1.64.0 driving Node playwright 1.64.1 and its Chromium, both pinned |
| Pixel diff | In Chromium, on an OffscreenCanvas; identical files skip it |
| Approvals | SHA-256 fingerprints in JSON |
| Viewer | One HTML file, plain JavaScript and CSS, no build step |
| Tests | Minitest through bin/test: unit tests plus a fixture app mapped twice end to end |