capybara-storyboard 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +11 -0
- data/docs/visual-regression.md +4 -1
- data/lib/capybara/storyboard/page_stability.rb +32 -2
- data/lib/capybara/storyboard/session.rb +52 -0
- data/lib/capybara/storyboard/test_helper.rb +1 -1
- data/lib/capybara/storyboard/version.rb +1 -1
- data/skills/visual-regression-test/SKILL.md +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bf6a851426d1aa00542d1d81071115072cd603519243887686156f178c62d57
|
|
4
|
+
data.tar.gz: 145f880b26d5d6ae6b8042f2434b398bb018f7d0b646cc5fc0922cd9ed25d509
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b73fed754aafe430f72452947b62f3ebb50480b3deeeea2c3dff00f6296d17297eaa96004c94d12908838dedbde58ac5ff966db331003e628fc3b639d9312129
|
|
7
|
+
data.tar.gz: 6f61ca195317e9c69f7cd476e158d55b48f86fc1d072a60829a93a780527176d94d0a5c1bf44cb5137e8ddbd6739cd7f4081d12f84c850e9991bcd39b6ac4a6f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- Replace wholly-numeric path segments in `visit` paths with `N` in screenshot filenames
|
|
6
|
+
(e.g. `visit '/observations/20/edit'` now produces `visit_observations_N_edit.png`), so
|
|
7
|
+
varying record IDs no longer break visual-regression pairing between runs. Migration note:
|
|
8
|
+
filenames change for numeric-ID visits, so the first run after upgrading shows one round of
|
|
9
|
+
added/deleted images when compared against an older baseline.
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-07-23
|
|
12
|
+
|
|
13
|
+
- Fix `NoMethodError` / spurious "skipped after error" warnings when a page navigation happens between setup and a stability poll, by treating non-finite poll results (nil/NaN/Infinity) as "measurement lost" and re-arming the observer
|
|
14
|
+
|
|
3
15
|
## [0.2.0] - 2026-07-22
|
|
4
16
|
|
|
5
17
|
- Add `visual-regression-test` skill for AI-assisted before/after screenshot comparison
|
data/README.md
CHANGED
|
@@ -171,6 +171,17 @@ order in which actions occurred. For example:
|
|
|
171
171
|
Non-ASCII descriptions and labels (e.g. Japanese) are preserved as-is in file and directory
|
|
172
172
|
names; only symbols and whitespace are replaced with underscores.
|
|
173
173
|
|
|
174
|
+
For `visit`, wholly-numeric path segments are additionally replaced with `N` before
|
|
175
|
+
sanitization, e.g. `visit '/observations/20/edit'` produces `001_visit_observations_N_edit.png`
|
|
176
|
+
rather than embedding the `20`. Record IDs vary between test runs and between branches, and a
|
|
177
|
+
filename that varies with them stops a visual-regression tool from pairing the same screen
|
|
178
|
+
across two runs, which reports it as added + deleted instead of comparing it. This only
|
|
179
|
+
replaces path segments that are *entirely* digits (`user42` and `h2` are left as-is), only
|
|
180
|
+
applies to `visit`, and only touches the path — the query string and fragment are untouched
|
|
181
|
+
(`/search?id=20` stays `search_id_20`). Locators passed to `click_on`, `fill_in`, and friends
|
|
182
|
+
keep their digits. If you want a specific ID to appear in a filename anyway, use
|
|
183
|
+
`storyboard_screenshot(label)` to build the label yourself.
|
|
184
|
+
|
|
174
185
|
The default output root is `<Rails.root>/tmp/screenshots` (overridable, see
|
|
175
186
|
[Configuration](#configuration)).
|
|
176
187
|
|
data/docs/visual-regression.md
CHANGED
|
@@ -96,7 +96,10 @@ The compare job uploads two artifacts:
|
|
|
96
96
|
output and would cause over-selection.
|
|
97
97
|
- A large batch of "added"/"deleted" images clustered under one example usually means a
|
|
98
98
|
Capybara step was inserted or removed in that example, shifting every later step's `NNN`
|
|
99
|
-
sequence number — not many independent regressions.
|
|
99
|
+
sequence number — not many independent regressions. Numeric `visit` path segments are
|
|
100
|
+
already normalized to `N` by capybara-storyboard, so a differing record ID there is not a
|
|
101
|
+
cause — a shifted `NNN` is usually the remaining explanation. (An ID in a query string, as
|
|
102
|
+
in `/search?user_id=42`, is not normalized and can still shift a filename.)
|
|
100
103
|
- Diff images are linked via artifact URLs rather than embedded in the PR comment, because
|
|
101
104
|
GitHub's Markdown image embedding requires a URL its camo proxy can fetch unauthenticated,
|
|
102
105
|
and artifact URLs require authentication.
|
|
@@ -62,8 +62,15 @@ module Capybara
|
|
|
62
62
|
stable = stable?(result, interval)
|
|
63
63
|
break if stable
|
|
64
64
|
|
|
65
|
-
# No point sleeping after the final check —
|
|
66
|
-
|
|
65
|
+
# No point re-arming or sleeping after the final check — cleanup runs
|
|
66
|
+
# immediately after and nothing re-checks it.
|
|
67
|
+
next if attempt == max_attempts - 1
|
|
68
|
+
|
|
69
|
+
# The measurement was reset by a page navigation (non-numeric result);
|
|
70
|
+
# re-arm the observer with the configured excluded animations so the
|
|
71
|
+
# next poll can measure again.
|
|
72
|
+
setup(page, excluded_animations) unless measurable?(result)
|
|
73
|
+
sleep(interval)
|
|
67
74
|
end
|
|
68
75
|
|
|
69
76
|
# Unstable (or max_attempts was 0): the page is deemed good enough.
|
|
@@ -106,9 +113,32 @@ module Capybara
|
|
|
106
113
|
nil
|
|
107
114
|
end
|
|
108
115
|
|
|
116
|
+
# True when the poll returned real numbers to compare. A page navigation
|
|
117
|
+
# between setup and a poll swaps in a fresh document whose
|
|
118
|
+
# window._lastMutationTime is undefined, so Date.now() - undefined === NaN.
|
|
119
|
+
# Callers use this to decide whether to re-arm the observer (Ruby side) and
|
|
120
|
+
# whether the numeric comparison in #stable? is even meaningful.
|
|
121
|
+
def measurable?(result)
|
|
122
|
+
finite_number?(result['runningAnimations']) && finite_number?(result['timeSinceLastMutation'])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# True only for a real, finite number. Guards against nil (Selenium
|
|
126
|
+
# serializes a navigation-reset NaN to JSON null) and against Float::NAN /
|
|
127
|
+
# Infinity (CDP drivers such as Cuprite/Ferrum decode the reset NaN back
|
|
128
|
+
# into a literal Float::NAN), both of which mean "the measurement was lost
|
|
129
|
+
# and must be re-armed".
|
|
130
|
+
def finite_number?(value)
|
|
131
|
+
value.is_a?(Numeric) && (!value.is_a?(Float) || value.finite?)
|
|
132
|
+
end
|
|
133
|
+
|
|
109
134
|
# evaluate_script returns string-keyed hashes on the real drivers; the
|
|
110
135
|
# DOM-quiet window is measured in ms, so compare against interval * 1000.
|
|
136
|
+
# A non-numeric result (e.g. the measurement was reset by a page
|
|
137
|
+
# navigation) is treated as "not stable yet", which also keeps this
|
|
138
|
+
# comparison free of NoMethodError regardless of what the driver hands back.
|
|
111
139
|
def stable?(result, interval)
|
|
140
|
+
return false unless measurable?(result)
|
|
141
|
+
|
|
112
142
|
result['runningAnimations'].zero? && result['timeSinceLastMutation'] >= (interval * 1000)
|
|
113
143
|
end
|
|
114
144
|
|
|
@@ -12,6 +12,9 @@ module Capybara
|
|
|
12
12
|
# of Capybara and RSpec hooks: a Session can be built with a plain example
|
|
13
13
|
# double and an injected +output_root+.
|
|
14
14
|
class Session
|
|
15
|
+
PATH_ID_PLACEHOLDER = 'N'
|
|
16
|
+
private_constant :PATH_ID_PLACEHOLDER
|
|
17
|
+
|
|
15
18
|
def initialize(example:, enabled:, output_root: nil)
|
|
16
19
|
@example = example
|
|
17
20
|
@enabled = enabled
|
|
@@ -34,6 +37,31 @@ module Capybara
|
|
|
34
37
|
capture_with_label(page, label)
|
|
35
38
|
end
|
|
36
39
|
|
|
40
|
+
# Automatic screenshot for a path-shaped DSL action (currently only
|
|
41
|
+
# #visit). Same as #auto except wholly-numeric path segments in the detail
|
|
42
|
+
# are collapsed to N first, so /observations/20/edit and
|
|
43
|
+
# /observations/33/edit yield one stable filename. Record ids vary between
|
|
44
|
+
# runs and between branches; a varying filename stops a visual-regression
|
|
45
|
+
# tool from pairing the two sides by path, which reports the same screen
|
|
46
|
+
# as added + deleted instead of comparing it.
|
|
47
|
+
#
|
|
48
|
+
# Deliberately a separate entry point rather than a branch inside #auto:
|
|
49
|
+
# Session stays ignorant of which action names are path-shaped, and the
|
|
50
|
+
# caller declares it by choosing the method (as with #auto vs #manual).
|
|
51
|
+
# Locators passed to click_link/fill_in/... are human-written fixed
|
|
52
|
+
# strings, so they keep their digits and keep using #auto.
|
|
53
|
+
#
|
|
54
|
+
# The enabled/suppressed guards are repeated here rather than left to
|
|
55
|
+
# #auto because Ruby evaluates arguments before the call: without them,
|
|
56
|
+
# every visit in a disabled suite would normalize a path only for #auto to
|
|
57
|
+
# discard it, breaking the "disabled -> zero overhead" contract.
|
|
58
|
+
def auto_path(page, action, path)
|
|
59
|
+
return unless @enabled
|
|
60
|
+
return if suppressed?
|
|
61
|
+
|
|
62
|
+
auto(page, action, normalize_path_ids(path))
|
|
63
|
+
end
|
|
64
|
+
|
|
37
65
|
# Manual screenshot hook. Like #auto, captured only when enabled. For an
|
|
38
66
|
# unconditional screenshot, use Capybara's own save_screenshot. The page
|
|
39
67
|
# is passed explicitly rather than held as state.
|
|
@@ -144,6 +172,30 @@ module Capybara
|
|
|
144
172
|
def sanitize(text)
|
|
145
173
|
text.to_s.gsub(/[^\p{Word}-]/, '_').gsub(/_+/, '_').gsub(/\A_|_\z/, '')
|
|
146
174
|
end
|
|
175
|
+
|
|
176
|
+
# Replaces every wholly-numeric path segment with N (/observations/20/edit
|
|
177
|
+
# -> /observations/N/edit). Segment-based on purpose: only a segment that
|
|
178
|
+
# is *nothing but* digits is a record id, so user42, h2 and v2 are left
|
|
179
|
+
# alone.
|
|
180
|
+
#
|
|
181
|
+
# The query/fragment is split off and left untouched — an id there sits in
|
|
182
|
+
# a value (?id=20), not a segment, and rewriting it would need parameter
|
|
183
|
+
# parsing for no gain. Splitting on / with -1 keeps empty segments, so a
|
|
184
|
+
# trailing slash and a full URL's // both survive round-tripping (the host
|
|
185
|
+
# segment of http://example.com/x/20 is non-numeric, so a full URL needs
|
|
186
|
+
# no special case).
|
|
187
|
+
#
|
|
188
|
+
# Runs BEFORE #sanitize, which would otherwise collapse / into _ and make
|
|
189
|
+
# segment boundaries unrecoverable. \A\d+\z is ASCII-only by design: URL
|
|
190
|
+
# path ids are ASCII digits, and preserving Unicode is #sanitize's job.
|
|
191
|
+
def normalize_path_ids(path)
|
|
192
|
+
head, separator, tail = path.to_s.partition(/[?#]/)
|
|
193
|
+
normalized =
|
|
194
|
+
head.split('/', -1).map do |segment|
|
|
195
|
+
segment.match?(/\A\d+\z/) ? PATH_ID_PLACEHOLDER : segment
|
|
196
|
+
end
|
|
197
|
+
"#{normalized.join('/')}#{separator}#{tail}"
|
|
198
|
+
end
|
|
147
199
|
end
|
|
148
200
|
end
|
|
149
201
|
end
|
|
@@ -233,7 +233,9 @@ npx reg-cli tmp/vrt/after tmp/vrt/before tmp/vrt/diff \
|
|
|
233
233
|
directory). `1` means "there's a diff, go verify it," not failure.
|
|
234
234
|
- Since both sets ran the same spec, filenames match across the two sets and reg-cli pairs them
|
|
235
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
|
|
236
|
+
happens when the diff changed the spec's own steps (record IDs in `visit` **path segments** are
|
|
237
|
+
already normalized to `N` by the gem, so a differing ID there is not a cause — but an ID in a
|
|
238
|
+
query string, as in `/search?user_id=42`, is not normalized and can still shift a filename).
|
|
237
239
|
|
|
238
240
|
### 5. Verify the detected diffs
|
|
239
241
|
|