capybara-storyboard 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f2357cb5edac81b907bc4196d8469d85c5de0636932fa2dbe25d2985edaa6a9
4
- data.tar.gz: 2c4dffec621810ef339178aafacefffaefec409c3f6b72ef7c4944d1b770e7c3
3
+ metadata.gz: 3c4545f0de7b20b1fbfcf71a08ed644553d5445001370ff3d59a4d8c9fc9c88f
4
+ data.tar.gz: a3aa8204023cab2f89f1799f0fbf7aee7fe6965bbbe00d1944e75c27010dc158
5
5
  SHA512:
6
- metadata.gz: 57a736051c0a33c64227ae7f576d1c5b4739e8c3e1a9c5e969be1534abf1b1072463845a92bbff69b1396df8dbc0a5fa3fd4211c9412ed69133ca2d81a3cfe95
7
- data.tar.gz: c366ae904aada06f091ded0cb7697754506f40a5ceac87f6383a1a83cf0bda2f619e3a4901afc9c53690a399f5ace1a1064fcb6e7bc0b3e006b725e78e770f43
6
+ metadata.gz: 4aed98cd98f7137740c85d5b9d994adbec0ecd2f9c06c972c7caacc5b4087e7450710855e267d507651ceb1f5d4f4667bfb1bdf8d0a9d0f037c5d39f27bcadb4
7
+ data.tar.gz: f801d661ab48e2d980fc39aaa7f8d9614a79a6c6b3795c7335f33263658a872d97894c8e7ad76fabf72bd51767d2dfbbc17ad45c056bd86661c4c425e8d1d8b6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-07-22
4
+
5
+ - Add `visual-regression-test` skill for AI-assisted before/after screenshot comparison
6
+
3
7
  ## [0.1.0] - 2026-07-21
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -108,7 +108,9 @@ which test files are captured.
108
108
  | Selective capture | `1` | set via `SCREENSHOT_TESTS_FILE` / `SCREENSHOT_TESTS` | Only the listed test files are captured. If the resulting set is empty, zero screenshots are taken (an empty selection does NOT fall back to capturing everything). |
109
109
 
110
110
  In short: `SCREENSHOTS` arms the mechanism as a whole, and the target list — when present —
111
- filters down which tests are captured within that armed mechanism.
111
+ filters down which tests are captured within that armed mechanism. The target list affects
112
+ capture only, never test execution: RSpec still runs whatever you tell it to run (see
113
+ [Selecting which tests to capture](#selecting-which-tests-to-capture)).
112
114
 
113
115
  ## Selecting which tests to capture
114
116
 
@@ -126,14 +128,25 @@ If `SCREENSHOT_TESTS_FILE` points to a file that does not exist, `Capybara::Stor
126
128
  is raised. (This existence check only runs when `SCREENSHOTS` is enabled; when the mechanism
127
129
  is disabled, the target list is never read.)
128
130
 
131
+ > [!IMPORTANT]
132
+ > The target list narrows **which tests are captured**, not **which tests are run**. RSpec
133
+ > still executes every test you pass to it; the listed files are merely the ones that get
134
+ > screenshots. To also avoid running the other tests — the usual goal in CI — pass the target
135
+ > files to `rspec` as arguments instead of (or in addition to) using the target list. See the
136
+ > examples below.
137
+
129
138
  Examples:
130
139
 
131
140
  ```bash
132
- # Inline list of test files
141
+ # Inline list of test files (all system specs still RUN; only these two are CAPTURED)
133
142
  SCREENSHOTS=1 SCREENSHOT_TESTS=spec/system/login_spec.rb,spec/system/signup_spec.rb bundle exec rspec
134
143
 
135
144
  # List generated into a file (e.g. by CI, listing only changed specs)
136
145
  SCREENSHOTS=1 SCREENSHOT_TESTS_FILE=tmp/screenshot_targets.txt bundle exec rspec
146
+
147
+ # Narrow both execution AND capture: pass the files to rspec as arguments.
148
+ # With SCREENSHOTS=1, every test that actually runs is captured, so no target list is needed.
149
+ SCREENSHOTS=1 bundle exec rspec $(cat tmp/screenshot_targets.txt)
137
150
  ```
138
151
 
139
152
  ## Output layout
@@ -232,16 +245,23 @@ is still taken. The wait is tunable via:
232
245
  - Test files are expected to be laid out flat as `spec/system/*_spec.rb`. Nested
233
246
  subdirectories may work but are not comprehensively verified in this initial version.
234
247
  - The target-list selection matches on test file paths. A PR that changes only views
235
- (leaving the system spec file itself unchanged) cannot be picked up by the target-list
236
- approach this is a known, deliberate limitation of the initial version.
248
+ (leaving the system spec file itself unchanged) cannot be picked up by the basic
249
+ path-diff recipe in [docs/github-actions.md](docs/github-actions.md) a known, deliberate
250
+ limitation of that recipe. The [docs/visual-regression.md](docs/visual-regression.md)
251
+ workflow lifts it via optional Claude-driven target expansion.
237
252
 
238
253
  ## Guides
239
254
 
240
255
  - [docs/github-actions.md](docs/github-actions.md) — a recipe for capturing screenshots on a
241
256
  diff basis in GitHub Actions.
257
+ - [docs/visual-regression.md](docs/visual-regression.md) — a reusable GitHub Actions workflow
258
+ for AI-assisted visual regression testing.
242
259
  - [skills/capybara-storyboard](skills/capybara-storyboard) — an agent skill for visually
243
260
  verifying captured screenshots (Claude Code, or any agent that can read PNG files from the
244
261
  filesystem).
262
+ - [skills/visual-regression-test](skills/visual-regression-test) — an agent skill that
263
+ compares before/after screenshots with `reg-cli` to catch unintended visual regressions,
264
+ invoked via `/visual-regression-test [ref]` in Claude Code.
245
265
 
246
266
  ## Development
247
267
 
@@ -0,0 +1,106 @@
1
+ # Reusable GitHub Actions workflow: AI-assisted visual regression testing
2
+
3
+ This is a reusable workflow, not a copy-paste recipe: capture screenshots on the PR base and
4
+ head branches in parallel, pixel-diff them with [reg-cli](https://github.com/reg-viz/reg-cli),
5
+ have Claude classify each changed image as intended, a likely regression, or uncertain, and
6
+ report the result as a PR comment and job summary. It never blocks the PR — a human makes the
7
+ final call.
8
+
9
+ This complements the [`docs/github-actions.md`](github-actions.md) recipe rather than
10
+ replacing it. That recipe only captures screenshots and uploads them for manual review; it
11
+ doesn't detect what changed, and it can't pick up a PR that only touches views (not the spec
12
+ files themselves). This workflow adds automatic change detection on top, and — via optional
13
+ Claude-driven target expansion — can also capture specs affected by a view-only PR that never
14
+ touches a spec file.
15
+
16
+ ## Usage
17
+
18
+ Call it from a workflow in your application repository. Pin the `uses:` ref to one that
19
+ actually exists — the example below uses `@main`; a release tag such as `@v0.1.0`, or a
20
+ `@v1` major tag once one is published, work too (a ref that doesn't resolve fails with
21
+ "workflow was not found"):
22
+
23
+ ```yaml
24
+ name: Visual regression
25
+
26
+ on:
27
+ pull_request:
28
+
29
+ jobs:
30
+ visual-regression:
31
+ # A called workflow's token permissions are the intersection of what it
32
+ # requests and what the caller grants, so grant pull-requests: write here —
33
+ # otherwise the PR-comment step is denied (403) on repositories whose default
34
+ # workflow token is read-only.
35
+ permissions:
36
+ contents: read
37
+ pull-requests: write
38
+ uses: aki77/capybara-storyboard/.github/workflows/visual-regression.yml@main
39
+ with:
40
+ ruby_version: "3.4"
41
+ secrets:
42
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
43
+ ```
44
+
45
+ ### Inputs
46
+
47
+ | Input | Default | Description |
48
+ |---|---|---|
49
+ | `base_ref` | `${{ github.event.pull_request.base.ref }}` | Base ref to compare against (the expected side). |
50
+ | `ruby_version` | `"3.4"` | Ruby version passed to `ruby/setup-ruby`. |
51
+ | `node_version` | `"20"` | Node.js version for reg-cli (requires Node 20+). |
52
+ | `reg_threshold` | `"0"` | `reg-cli --thresholdRate` (0..1). Fraction of changed pixels tolerated before an image counts as changed. |
53
+ | `enable_claude` | `true` | Have Claude classify the changed diff images (intended / regression / uncertain). |
54
+ | `enable_claude_target_expansion` | `true` | Have Claude expand the target spec list from view-facing changes (view/template/component/CSS). |
55
+ | `model` | `""` | Model to pass to `claude-code-action` (via `--model`). Empty uses the action default. |
56
+ | `spec_paths` | `"system,components"` | Comma-separated spec subdirectories under `spec/` to capture (e.g. `system,components`). |
57
+
58
+ ### Secrets
59
+
60
+ | Secret | Required | Description |
61
+ |---|---|---|
62
+ | `anthropic_api_key` | No | Anthropic API key for `claude-code-action`. Only needed when Claude features are enabled. |
63
+
64
+ ## How it works
65
+
66
+ The workflow runs four jobs:
67
+
68
+ 1. **targets** — diffs `base_ref` against head to find changed spec files under `spec_paths`,
69
+ optionally asks Claude to expand that list from view-facing changes elsewhere in the diff,
70
+ and uploads the final list as an artifact.
71
+ 2. **shoot-base** / **shoot-head** — each checks out its own ref, runs only the target specs
72
+ with `SCREENSHOTS=1`, and uploads the resulting screenshots. Both depend only on `targets`,
73
+ so **they run in parallel**.
74
+ 3. **compare** — downloads both screenshot sets, pixel-diffs them with reg-cli, has Claude
75
+ classify the changed images (when enabled), and posts a PR comment and job summary. This
76
+ check never blocks the PR.
77
+
78
+ ## Reading the artifacts
79
+
80
+ The compare job uploads two artifacts:
81
+
82
+ - **`vr-flagged`** — only the diff images Claude classified as `regression` or `uncertain`,
83
+ uploaded uncompressed so the Actions UI can preview each PNG natively. Start here: it's
84
+ the small set of images that actually need a human look.
85
+ - **`vr-full`** — `report.html`, `reg.json`, and the full `diff` image set, as a normal zip.
86
+ Download and open `report.html` locally for the complete reg-cli report, or to inspect a
87
+ diff image that wasn't flagged. The base and head screenshots themselves are the separate
88
+ **`vr-shots-base`** / **`vr-shots-head`** artifacts.
89
+
90
+ ## Notes
91
+
92
+ - Set `enable_claude: false` to disable Claude classification entirely (reg-cli diffing still
93
+ runs); `anthropic_api_key` is not required in that case.
94
+ - Claude's target-list expansion starts only from view-facing changes (views, templates,
95
+ components, CSS) — never from model/migration changes, which usually don't affect rendered
96
+ output and would cause over-selection.
97
+ - A large batch of "added"/"deleted" images clustered under one example usually means a
98
+ Capybara step was inserted or removed in that example, shifting every later step's `NNN`
99
+ sequence number — not many independent regressions.
100
+ - Diff images are linked via artifact URLs rather than embedded in the PR comment, because
101
+ GitHub's Markdown image embedding requires a URL its camo proxy can fetch unauthenticated,
102
+ and artifact URLs require authentication.
103
+ - If a target spec fails on either branch, the shoot job goes red and the compare job is
104
+ skipped, so no diff report is produced. Fix the failing spec first — the workflow does not
105
+ diff against a partial screenshot set. ("Never blocks the PR" refers to the compare job not
106
+ failing the check once it runs, not to tolerating a broken spec run.)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Storyboard
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -0,0 +1,60 @@
1
+ # About the visual-regression-test skill
2
+
3
+ Compares UI changes (views, components, CSS, system specs) before/after using
4
+ capybara-storyboard screenshots and `reg-cli` image diffing, to catch unintended
5
+ visual regressions. Invoked via `/visual-regression-test [ref]`.
6
+
7
+ - "After" screenshots are taken from the main repo's working tree (including
8
+ uncommitted changes).
9
+ - "Before" screenshots are taken from a separate worktree
10
+ (`.claude/worktrees/vrt-baseline`) checked out at the baseline ref (explicit
11
+ argument, HEAD, or the base branch — see `scripts/resolve-baseline.sh` for
12
+ details).
13
+ - The two image sets are diffed with `reg-cli` and rendered as an HTML report.
14
+ The agent verifies whether each diff is intentional before reporting to the
15
+ user.
16
+
17
+ See `SKILL.md` for the full procedure, baseline resolution logic, and target
18
+ spec selection. This README only covers the setup needed to pass step 3b
19
+ (the startup check inside the worktree).
20
+
21
+ ## Setup on worktree creation
22
+
23
+ In `SKILL.md` step 3b, whether to proceed with capturing the "before" set is
24
+ decided solely by whether `bundle exec rails runner 'exit 0'` (or an
25
+ equivalent startup command) succeeds inside the worktree.
26
+
27
+ This **assumes the project is configured to automatically provision the
28
+ environment whenever `git worktree add` runs**. Gitignored config files (e.g.
29
+ DB connection settings, credentials) must be copied, and dependencies must be
30
+ installed, by the time the worktree is created — otherwise the startup check
31
+ fails.
32
+
33
+ How this is achieved doesn't matter. A common approach is a `post-checkout`
34
+ hook. Since `post-checkout` fires on both regular checkouts and
35
+ `git worktree add`, distinguish them using the `$1` argument (PREV_HEAD) Git
36
+ passes in: it's the all-zero SHA only for `git worktree add`. Also note that
37
+ if `core.hooksPath` isn't configured, `post-checkout` won't fire at all, even
38
+ from a custom hooks directory.
39
+
40
+ ```sh
41
+ #!/bin/sh
42
+ # .githooks/post-checkout
43
+ [ "$1" = "0000000000000000000000000000000000000000" ] || exit 0 # worktree add only
44
+
45
+ main=$(realpath "$(git rev-parse --git-common-dir)/..")
46
+ copy_if_missing() {
47
+ [ -f "$1" ] && return 0
48
+ [ -f "$main/$1" ] || return 0
49
+ mkdir -p "$(dirname "$1")"; cp "$main/$1" "$1"
50
+ }
51
+ copy_if_missing config/database.yml # add any other required config files similarly
52
+
53
+ [ -d vendor/bundle ] || bundle install
54
+ [ -d node_modules ] || npm install
55
+ # if the DB is shared with the main repo, don't call db:prepare or similar
56
+ ```
57
+
58
+ Which files to copy and which install commands to run varies per project.
59
+ Identify which gitignored files actually cause the startup command to fail
60
+ inside the worktree, and adapt accordingly.
@@ -0,0 +1,304 @@
1
+ ---
2
+ name: visual-regression-test
3
+ disable-model-invocation: true
4
+ description: >-
5
+ Compare before/after UI changes (views, components, CSS, system specs)
6
+ using capybara-storyboard screenshots and reg-cli image diffing, to catch
7
+ unintended visual regressions. The comparison baseline is determined
8
+ automatically from an optional ref argument, the presence of uncommitted
9
+ changes, and the base branch. Invoke explicitly with
10
+ /visual-regression-test [ref].
11
+ ---
12
+
13
+ # Visual regression test (before / after screenshot diff)
14
+
15
+ Catch unintended visual changes introduced by uncommitted edits. Capture capybara-storyboard
16
+ screenshots **twice** — once against the main repo's working tree ("after"), and once against a
17
+ separate worktree checked out at a baseline ref (HEAD, an explicit ref, or the merge-base with
18
+ the base branch — "before") — then diff the two image sets with `reg-cli`. The output is an HTML
19
+ report that highlights which screens changed, so both the agent and the user can confirm whether
20
+ each change was intentional.
21
+
22
+ This differs from "reviewing a screenshot of the current code as-is." A single-snapshot review
23
+ answers "does this screen look right right now?" This skill answers "which screens changed
24
+ compared to before my edit, and was that change intentional?" The latter is what actually catches
25
+ regressions caused by editing a shared partial or layout or CSS. If there's no baseline to compare
26
+ against and the user just wants to look at the current UI, that's the territory of the plain
27
+ `capybara-storyboard` screenshot review, not this skill.
28
+
29
+ Because the "before" capture runs in a separate worktree, it never touches the user's original
30
+ working tree. There's no stash-and-restore step, and therefore no structural risk of losing the
31
+ user's uncommitted work by failing to restore it.
32
+
33
+ ## Prerequisites (confirm before starting)
34
+
35
+ This skill assumes capybara-storyboard is already set up (screenshots are captured with
36
+ `SCREENSHOTS=1` and land under `tmp/screenshots/`). Setting it up is out of scope for this skill.
37
+
38
+ - **You must be able to determine what to compare against (the baseline).** Baseline resolution
39
+ is delegated to `scripts/resolve-baseline.sh` (see "Determining the baseline" below and step
40
+ 3a). If a ref argument was passed, that ref is always the baseline; with no argument and
41
+ uncommitted changes present, HEAD is the baseline; with no argument and a clean tree, the base
42
+ branch is the baseline. So "the tree is clean" is not itself a reason to abort — no-argument
43
+ plus clean proceeds to a comparison against the base branch (effectively a three-dot diff
44
+ against the merge-base). Only abort when the baseline genuinely can't be resolved
45
+ (`resolve-baseline.sh` exits 2).
46
+ - **reg-cli** runs via `npx reg-cli` (no install needed). Open the report with the platform's
47
+ `open` command (macOS).
48
+
49
+ ## Determining the baseline (the "before" worktree's starting ref)
50
+
51
+ "After" is always the main repo's working tree as-is (including any uncommitted changes — the
52
+ after side is never turned into a worktree). Only the ref that the "before" worktree is checked
53
+ out at is decided, using this priority:
54
+
55
+ 1. A ref/branch was passed as an argument → use that ref (regardless of uncommitted changes).
56
+ 2. No argument, and uncommitted changes exist (`git status --short` is non-empty) → HEAD (the
57
+ original behavior).
58
+ 3. No argument, and the tree is clean → the base branch.
59
+
60
+ When the baseline is anything other than HEAD (case 1 or 3), the worktree's actual starting point
61
+ is `git merge-base <baseline> HEAD` (a three-dot diff). For HEAD, no merge-base step is needed —
62
+ use HEAD directly. This logic is centralized in the bundled `scripts/resolve-baseline.sh` (it
63
+ prints the starting ref to stdout on one line, and exits 2 if it can't be resolved). Resolving the
64
+ base branch itself is also handled by that script (roughly in the order
65
+ `github-pr-base-branch` → `vscode-merge-base` → `@{upstream}` → `origin/HEAD`; see
66
+ `resolve-baseline.sh` for the exact order and how each source is handled).
67
+
68
+ ### Interpreting the argument
69
+
70
+ Treat the first argument to `/visual-regression-test [ref]` as the baseline ref (any git ref: a
71
+ branch name, tag, SHA, `origin/foo`, etc.). Pass it through as the first argument to
72
+ `resolve-baseline.sh` (pass an empty string if no argument was given).
73
+
74
+ - No argument → baseline is determined automatically (uncommitted changes present = HEAD; clean
75
+ = base branch).
76
+ - One argument → that ref is fixed as the baseline.
77
+ - Two or more arguments → use only the first as the baseline, and briefly tell the user the rest
78
+ were ignored (this skill only supports a single baseline comparison; the range's starting point
79
+ is computed automatically via merge-base, so there's no need to accept both a start and an end).
80
+ - If the given ref can't be resolved, `resolve-baseline.sh` stops with exit 2. Tell the user that
81
+ ref doesn't exist and abort (don't silently fall back to a different ref — respect the explicit
82
+ choice).
83
+
84
+ ## Procedure
85
+
86
+ ### 1. Identify the target specs
87
+
88
+ Reuse the same target-selection logic as the plain `capybara-storyboard` review, narrowing to the
89
+ system specs that exercise the changed screen(s). Never run the entire suite twice — it's very
90
+ slow, and produces far too large a diff to read.
91
+
92
+ - If `app/views`, `app/components`, or CSS changed, find the system specs that render that
93
+ screen (search by view/component name, controller action, or route). If the call sites are
94
+ countable (roughly 10 or fewer), target those specs directly.
95
+ - If the change is to a layout or a widely shared partial/component, and the search above would
96
+ return most of the suite, don't enumerate every caller. Instead propose a small representative
97
+ sample (roughly 3-6 specs spanning distinct contexts: a signed-out screen, a standard
98
+ authenticated page, an admin/dense-UI page, a narrow-viewport spec if one exists), and **confirm
99
+ with the user** before running.
100
+ - If a system spec itself changed, that spec is the target. Note that if the diff changes the
101
+ spec's own steps, the before/after screenshot sequences won't correspond 1:1 — reg-cli will
102
+ report these as added/removed images, which is expected in this case.
103
+ - If only a single example in a file is relevant, it's fine to scope down to that example (`-e` or
104
+ a line number).
105
+
106
+ Record the exact `rspec` target arguments (files and/or line numbers), since the same command runs
107
+ twice and both runs must match.
108
+
109
+ Note that the target spec may not exist, or may differ, at the baseline ref (resolved as the
110
+ "before" starting point in step 3a). In no-argument mode with uncommitted changes (baseline=HEAD),
111
+ this only applies to newly added specs. In ref-specified or base-branch-comparison mode, the
112
+ target spec itself might be absent from the baseline, or have a different signature
113
+ (describe/example names). Handle that per step 3c and the "images present on only one side are
114
+ new/deleted" note in step 4. For target selection itself, just use the after side (the main repo's
115
+ working tree) as the reference.
116
+
117
+ This target argument works as-is in both the main repo and the worktree described below (the
118
+ worktree is a full checkout of the baseline ref, so its path layout matches the main repo).
119
+ However, if the target includes a spec file that doesn't exist at the baseline ref (a newly added
120
+ spec, or one not yet present when comparing against a base branch), the worktree run will either
121
+ fail to load it or produce an empty "before" set with all images treated as new. This is expected,
122
+ not an error. In base-branch-comparison mode (no argument and clean, or an older explicit ref),
123
+ even when the target spec exists at the baseline its contents may differ, so the screenshot count
124
+ or naming may not match "after". reg-cli will report these as diffs/additions/deletions, which is
125
+ also expected.
126
+
127
+ ### 2. Capture the "after" set (main repo's working tree)
128
+
129
+ Capture the after state (with the diff applied) first. This only touches the main repo, so you
130
+ can confirm the target specs actually pass before creating a worktree. If this fails, no worktree
131
+ is created and no work is wasted.
132
+
133
+ ```bash
134
+ SCREENSHOTS=1 bundle exec rspec <target specs>
135
+ mkdir -p tmp/vrt && rm -rf tmp/vrt/after
136
+ cp -R tmp/screenshots tmp/vrt/after
137
+ ```
138
+
139
+ If the target spec **fails**, the screenshot set may be incomplete. Report the failure (the
140
+ failure itself is a finding), and confirm with the user whether it's acceptable to use the partial
141
+ capture as the diff baseline.
142
+
143
+ ### 3. Create a worktree at the baseline, verify it boots, and capture "before"
144
+
145
+ #### 3a. Resolve the baseline and create the worktree there
146
+
147
+ Determine the worktree's starting ref using the logic in "Determining the baseline". Pass the
148
+ argument received by this invocation (the baseline ref, or empty if none) straight through to
149
+ `resolve-baseline.sh`.
150
+
151
+ ```bash
152
+ WT=.claude/worktrees/vrt-baseline
153
+ # Pass "" if no argument was given. Prints the starting ref/SHA to stdout on one line. Abort on exit 2.
154
+ BEFORE_REF=$(.claude/skills/visual-regression-test/scripts/resolve-baseline.sh "${BASELINE_ARG:-}")
155
+ git worktree add "$WT" "$BEFORE_REF"
156
+ ```
157
+
158
+ `.claude/worktrees/` is an existing empty directory reserved for this skill's use.
159
+ `resolve-baseline.sh` writes the resolved mode (explicit / HEAD / base branch), and — when it's
160
+ not HEAD — the merge-base it applied, to **stderr**. Always mention what was used as the baseline
161
+ in the user-facing report (step 6) (e.g. "compared against the merge-base with the base branch
162
+ origin/release-candidate"), since it changes how the diff should be read.
163
+
164
+ When the starting point is something other than HEAD (an explicit ref or the base branch), the
165
+ worktree is a full checkout of that SHA/branch. Note that the diff shown as "after minus before"
166
+ then includes both the uncommitted changes in the after-side working tree *and* every commit
167
+ between that starting point and HEAD — not just the uncommitted diff, as it would be when the
168
+ baseline is HEAD.
169
+
170
+ #### 3b. Detect environment setup (important — this is what makes the skill portable)
171
+
172
+ Verify whether Rails actually boots inside the worktree by checking boot success itself, rather
173
+ than checking for the presence of any specific hook. Judging by "does it boot" rather than by
174
+ project-specific mechanics keeps this step portable to other projects.
175
+
176
+ ```bash
177
+ ( cd "$WT" && bundle exec rails runner 'exit 0' )
178
+ ```
179
+
180
+ If it boots successfully, treat the config files and dependencies as present (either the
181
+ worktree's automatic setup handled it, or none was needed) and proceed to 3c.
182
+
183
+ If boot fails, abort. Tell the user "this worktree is missing config files/dependencies — you need
184
+ automatic setup on worktree creation (e.g. a post-checkout hook)," and point them at this
185
+ directory's `README.md` (the reference implementation for this skill). Then clean up the worktree
186
+ before finishing.
187
+
188
+ ```bash
189
+ git worktree remove --force "$WT"
190
+ ```
191
+
192
+ #### 3c. Capture "before" inside the worktree
193
+
194
+ Run the identical rspec command inside the worktree, and collect the results into the main repo's
195
+ `tmp/vrt/before`.
196
+
197
+ ```bash
198
+ ( cd "$WT" && SCREENSHOTS=1 bundle exec rspec <target specs> )
199
+ rm -rf tmp/vrt/before
200
+ cp -R "$WT/tmp/screenshots" tmp/vrt/before
201
+ ```
202
+
203
+ Running inside a `( cd "$WT" && ... )` subshell keeps the main repo's current directory clean. If
204
+ the target spec fails, handle it the same way as "after" in step 2 (report the failure, and
205
+ confirm with the user whether to use the partial capture).
206
+
207
+ ### 4. Discard the worktree and diff with reg-cli in the main repo
208
+
209
+ Once "before" is captured, the worktree is no longer needed — discard it, then diff in the main
210
+ repo.
211
+
212
+ ```bash
213
+ git worktree remove --force "$WT"
214
+ .claude/skills/visual-regression-test/scripts/reg-diff.sh tmp/vrt/after tmp/vrt/before tmp/vrt
215
+ ```
216
+
217
+ `--force` is used because any untracked `tmp/screenshots` left inside the worktree doesn't matter
218
+ by this point (it was already collected into `before` in step 3c) — just remove it.
219
+
220
+ The bundled helper (`reg-diff.sh`) is equivalent to:
221
+
222
+ ```bash
223
+ npx reg-cli tmp/vrt/after tmp/vrt/before tmp/vrt/diff \
224
+ --report tmp/vrt/report.html \
225
+ --json tmp/vrt/report.json
226
+ ```
227
+
228
+ - Argument order matters: **actual (after) first, expected (before) second, diff directory
229
+ third.** Treating after as actual and before as expected frames the report as "what did my diff
230
+ do relative to HEAD."
231
+ - Script exit codes: `0` for no visual change, `1` when a diff is detected (expected and
232
+ **not an error** in this context), `2` for an execution error (bad arguments / missing input
233
+ directory). `1` means "there's a diff, go verify it," not failure.
234
+ - Since both sets ran the same spec, filenames match across the two sets and reg-cli pairs them
235
+ automatically by path. Images present on only one side are reported as added/deleted — this
236
+ happens when the diff changed the spec's own steps.
237
+
238
+ ### 5. Verify the detected diffs
239
+
240
+ Read `tmp/vrt/report.json` to get the list of changed/added/deleted images, and for each changed
241
+ screen, read the actual **diff image** (`tmp/vrt/diff/**`) along with the corresponding before/
242
+ after PNGs. Use the filename to determine which action/screen it belongs to
243
+ (`tmp/vrt/before/{Group}/{example}/{NNN_action}.png`). All paths are under the main repo's
244
+ `tmp/vrt`.
245
+
246
+ For each changed image, judge whether the visual change is **intentional**.
247
+
248
+ - Does the visual change match what the code diff was trying to do? (e.g. a color/spacing/label
249
+ change the user made → expected.)
250
+ - Or did an unrelated screen shift — a shared partial/layout edit leaking into pages it shouldn't
251
+ affect, broken layout, overflow, a rendering difference in a component? → report this as a
252
+ regression.
253
+ - Minor sub-pixel/anti-aliasing noise with no meaningful change → note it as likely noise rather
254
+ than a regression. If noise dominates, mention that reg-cli's threshold options
255
+ (`--threshold`, `--enableAntialias`, etc.) can suppress it on a re-run.
256
+
257
+ ### 6. Report, then open the HTML report for the user
258
+
259
+ Only open reg-cli's HTML report after finishing the verification in step 5. Opening it earlier
260
+ would show the user diffs the agent hasn't judged yet, breaking the "agent verifies first, then
261
+ presents results to the user" order.
262
+
263
+ ```bash
264
+ open tmp/vrt/report.html
265
+ ```
266
+
267
+ Then return a concise written report:
268
+
269
+ - How many screens changed/were added/were deleted (from `report.json`).
270
+ - For each meaningful change: which screen (spec + action/filename), and your judgment —
271
+ **intentional** (matches the code change) or **possible regression** (what looks wrong and
272
+ where).
273
+ - A clear conclusion: either "all detected changes are consistent with the intended edit," or
274
+ "the following look like unintended regressions: ...".
275
+ - Mention that the HTML report is open in the browser so the user can check it themselves.
276
+
277
+ ### 7. Output and cleanup
278
+
279
+ By step 4, the worktree is guaranteed to already be removed via `git worktree remove --force`
280
+ (the same applies if step 3b aborted — it's cleaned up before finishing there too). If for some
281
+ reason execution was interrupted and the worktree was left behind, check with
282
+ `git worktree list` for a stray `.claude/worktrees/vrt-baseline` and remove it with
283
+ `git worktree remove --force <path>`.
284
+
285
+ The worktree shares the same DB (`config/database.yml`) as the main repo. Don't run other tests
286
+ concurrently while this skill is executing (steps 2-4), since DB state can conflict.
287
+
288
+ When the baseline is a base branch or an older ref, that ref's Rails may be out of sync with the
289
+ current shared DB schema (migrations already applied in the main repo), causing the worktree's
290
+ boot check (step 3b) or rspec run to fail. This is an expected failure caused by an outdated
291
+ baseline, and the boot check is what catches it before it goes further. If this happens
292
+ frequently, suggest the user shift the baseline closer to HEAD (e.g. compare against uncommitted
293
+ changes only).
294
+
295
+ This skill's final step is "let the user see the HTML report," so **leave the report and the diff
296
+ images it references (`tmp/vrt/`) in place**. Deleting them here would pull the report out from
297
+ under the user while they're looking at it. Don't clean up on the agent's side.
298
+
299
+ Just mention that these can be removed once no longer needed (leave the decision of when to run
300
+ this to the user):
301
+
302
+ ```bash
303
+ rm -rf tmp/vrt tmp/screenshots
304
+ ```
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Diff a before/after set of screenshots using reg-cli.
3
+ # Fixes reg-cli's argument order (actual -> expected -> diff) and report paths
4
+ # to prevent mix-ups on the caller's side.
5
+ #
6
+ # NOTE: does not open the report. The agent verifies report.json / diff images
7
+ # first, and opening it for the user happens afterward (that step is done
8
+ # explicitly in SKILL.md step 6).
9
+ #
10
+ # Usage:
11
+ # scripts/reg-diff.sh <after_dir> <before_dir> [out_dir]
12
+ # after_dir : screenshot directory for after (working tree) = reg-cli's actual
13
+ # before_dir : screenshot directory for before (HEAD) = reg-cli's expected
14
+ # out_dir : output destination (default tmp/vrt). diff/report.html/report.json written here
15
+ #
16
+ # Exit codes:
17
+ # 0 ... no diff (no visual change)
18
+ # 1 ... diff detected (expected in this skill's flow, not an error)
19
+ # 2 ... execution error such as invalid arguments or missing input directory
20
+
21
+ set -u
22
+
23
+ after_dir=${1:-}
24
+ before_dir=${2:-}
25
+ out_dir=${3:-tmp/vrt}
26
+
27
+ if [[ -z "$after_dir" || -z "$before_dir" ]]; then
28
+ echo "usage: reg-diff.sh <after_dir> <before_dir> [out_dir]" >&2
29
+ exit 2
30
+ fi
31
+ for d in "$after_dir" "$before_dir"; do
32
+ if [[ ! -d "$d" ]]; then
33
+ echo "error: directory not found: $d" >&2
34
+ exit 2
35
+ fi
36
+ done
37
+
38
+ diff_dir="$out_dir/diff"
39
+ report_html="$out_dir/report.html"
40
+ report_json="$out_dir/report.json"
41
+ mkdir -p "$diff_dir"
42
+
43
+ # Pass args in the order actual(after) -> expected(before) -> diff.
44
+ npx reg-cli "$after_dir" "$before_dir" "$diff_dir" \
45
+ --report "$report_html" \
46
+ --json "$report_json"
47
+ reg_status=$?
48
+
49
+ # reg-cli exits non-zero (1) when a diff is detected. That's expected, so don't abort here.
50
+ # Don't open the report (opened in step 6, after verification).
51
+ echo "---"
52
+ echo "reg-cli exit=$reg_status (0=no diff, 1=diff detected)"
53
+ echo "report.html: $report_html # open this after verification to check"
54
+ echo "report.json: $report_json"
55
+ echo "diff images: $diff_dir"
56
+
57
+ exit "$reg_status"
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # Resolve the starting ref for the "before" worktree according to a priority order.
3
+ #
4
+ # Priority:
5
+ # 1. <ref> argument given -> use that ref as baseline
6
+ # 2. no argument & uncommitted changes present -> HEAD as baseline (current behavior)
7
+ # 3. no argument & clean tree -> base branch as baseline
8
+ #
9
+ # If baseline is anything other than HEAD, the worktree starting point is
10
+ # merge-base <baseline> HEAD (equivalent to a three-dot diff).
11
+ # If it's HEAD, no merge-base is needed and "HEAD" is returned as-is.
12
+ #
13
+ # stdout : one line with the ref/SHA to pass to worktree add
14
+ # stderr : human-readable explanation of the resolution
15
+ # exit : 0=resolved, 2=execution error
16
+
17
+ set -u
18
+
19
+ arg_ref=${1:-}
20
+
21
+ log() { echo "$@" >&2; }
22
+
23
+ # Check whether a ref actually exists (existence check only, no side effects)
24
+ ref_exists() { git rev-parse --verify --quiet "$1" >/dev/null; }
25
+
26
+ # Resolve the base branch (order follows github.com/aki77/claude-code-review as reference)
27
+ resolve_base_branch() {
28
+ local cur cfg branch cand upstream sym
29
+ cur=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
30
+
31
+ # 1. branch.<cur>.github-pr-base-branch
32
+ # NOTE: the value may be in "owner#repo#branch" form (VSCode GitHub PR extension).
33
+ # Extract the last '#' segment as the branch name and prefer origin/.
34
+ # The raw value can't be passed to rev-parse as-is.
35
+ cfg=$(git config "branch.$cur.github-pr-base-branch" 2>/dev/null)
36
+ if [ -n "$cfg" ]; then
37
+ branch=${cfg##*#}
38
+ for cand in "origin/$branch" "$branch" "$cfg"; do
39
+ if ref_exists "$cand"; then echo "$cand"; return 0; fi
40
+ done
41
+ fi
42
+
43
+ # 2. branch.<cur>.vscode-merge-base (e.g. origin/release-candidate; usable as-is)
44
+ cfg=$(git config "branch.$cur.vscode-merge-base" 2>/dev/null)
45
+ if [ -n "$cfg" ] && ref_exists "$cfg"; then
46
+ echo "$cfg"; return 0
47
+ fi
48
+
49
+ # 3. @{upstream}
50
+ upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)
51
+ if [ -n "$upstream" ]; then
52
+ echo "$upstream"; return 0
53
+ fi
54
+
55
+ # 4. origin/HEAD (e.g. refs/remotes/origin/release-candidate)
56
+ sym=$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null)
57
+ if [ -n "$sym" ]; then
58
+ echo "${sym#refs/remotes/}"; return 0
59
+ fi
60
+
61
+ return 1
62
+ }
63
+
64
+ emit_mergebase() {
65
+ local baseline=$1 mb
66
+ if ! ref_exists "$baseline"; then
67
+ log "error: baseline ref not found: $baseline"
68
+ exit 2
69
+ fi
70
+ mb=$(git merge-base "$baseline" HEAD 2>/dev/null)
71
+ if [ -z "$mb" ]; then
72
+ log "error: no merge-base between $baseline and HEAD"
73
+ exit 2
74
+ fi
75
+ log "baseline: '$baseline' -> merge-base $mb"
76
+ echo "$mb"
77
+ }
78
+
79
+ # --- Priority resolution ---
80
+ if [ -n "$arg_ref" ]; then
81
+ log "baseline mode: explicit ref '$arg_ref'"
82
+ emit_mergebase "$arg_ref"
83
+ exit 0
84
+ fi
85
+
86
+ if [ -n "$(git status --short)" ]; then
87
+ log "baseline mode: uncommitted changes present -> HEAD"
88
+ echo "HEAD"
89
+ exit 0
90
+ fi
91
+
92
+ log "baseline mode: clean tree -> base branch"
93
+ base=$(resolve_base_branch) || { log "error: could not resolve base branch"; exit 2; }
94
+ log "resolved base branch: $base"
95
+ emit_mergebase "$base"
96
+ exit 0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-storyboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aki
@@ -38,6 +38,7 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - docs/github-actions.md
41
+ - docs/visual-regression.md
41
42
  - lib/capybara/storyboard.rb
42
43
  - lib/capybara/storyboard/configuration.rb
43
44
  - lib/capybara/storyboard/context.rb
@@ -49,6 +50,10 @@ files:
49
50
  - lib/capybara/storyboard/version.rb
50
51
  - sig/capybara/storyboard.rbs
51
52
  - skills/capybara-storyboard/SKILL.md
53
+ - skills/visual-regression-test/README.md
54
+ - skills/visual-regression-test/SKILL.md
55
+ - skills/visual-regression-test/scripts/reg-diff.sh
56
+ - skills/visual-regression-test/scripts/resolve-baseline.sh
52
57
  homepage: https://github.com/aki77/capybara-storyboard
53
58
  licenses:
54
59
  - MIT