ui_guardrails 1.5.0 → 1.6.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/doc/ROADMAP.md +2 -1
- data/doc/VISUAL-DIFF.md +16 -0
- data/lib/guardrails/configuration.rb +6 -7
- data/lib/guardrails/report/finding.rb +11 -1
- data/lib/guardrails/version.rb +1 -1
- data/lib/guardrails/visual_diff.rb +58 -12
- 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: a0bae61006af0fcdb91d543ab37539ed5f01bc653faa3a1732b56ee4f450d075
|
|
4
|
+
data.tar.gz: ee1b11b2668ee764377741beeac44b50e9507029c1469b5db866f1aa1e61ac93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe558c786b8286a5f5766c265e1b40b581ef139d05a4b989744ee6d98e1b2dc5d885fc9c1470853d393b23d9c10d99be70c5a1cd7127da6afab653aa0bee21ee
|
|
7
|
+
data.tar.gz: 89651a0654dcd990829f37e402c9a7b8d214573acf655c54c7460a3708cf7d6d1c8e4f34bdae064fb8b676faa0d02d96fa624d94c8bec492cedc86e967973e86
|
data/doc/ROADMAP.md
CHANGED
|
@@ -132,7 +132,8 @@ The planned roadmap ended at V2. Running the gem on real codebases (Patchvault:
|
|
|
132
132
|
| Emoji + glyph icon detection | ✅ shipped (1.5.0) | `Guardrails::Icons::EmojiScan` catches emoji pictographs and Unicode-dingbat glyphs used as UI iconography — grapheme-cluster iteration (VS16 / ZWJ / flag / keycap count as one), range-first single-codepoint classification, per-file-type comment masking (Prism for Ruby, `<%# %>` for ERB, `//` `/* */` for JS/SCSS, `#` for YAML), `guardrails-ok: emoji` inline marker (first inline-escape convention in the gem), `allow_files` + `allow_chars` config, `SUGGEST=1` sprite-name hints. `guardrails:icons` also flipped to blocking — exit 1 on inline SVGs, unknown sprite refs, or emoji icons. |
|
|
133
133
|
| Lookbook panel on the new data layer | not started | The auto-registered panel still renders from its own `ComponentReport`; it could render `Report::Finding`s and pick up suggestions for free. Icons findings could also flow through — the panel doesn't list icon findings today. |
|
|
134
134
|
| Icons on the new data layer | not started | Icons stays outside `Report::Run` — its rake task calls `Icons.new(root:).run` directly. Migrating gets it into the TUI + HTML report for free but changes the rake shape; hold until there's demand. |
|
|
135
|
-
|
|
|
135
|
+
| Visual-diff adapter registry | ✅ shipped (1.6.0) | `Guardrails::VisualDiff.register_adapter` lets other gems add adapters; the name is checked when the adapter is built. `VisualDiff::Finding` gains `status`, `comment`, `regions`, `extra_details`. First consumer: ui_guardrails-browser's `:browser` adapter. |
|
|
136
|
+
| CI test workflow | ✅ shipped | `.github/workflows/test.yml` runs `bundle exec rspec` (including the `examples/demo` integration spec) and `gem build` on every pull request and push to `main`, on Ruby 3.4 (the floor) and 4.0. Floor regressions and packaging mistakes surface on the PR instead of at tag time. |
|
|
136
137
|
|
|
137
138
|
---
|
|
138
139
|
|
data/doc/VISUAL-DIFF.md
CHANGED
|
@@ -61,6 +61,22 @@ With `enabled = true`, `rake guardrails:audit` always runs the visual-diff check
|
|
|
61
61
|
|
|
62
62
|
Env always overrides Configuration.
|
|
63
63
|
|
|
64
|
+
## Adapters from other gems
|
|
65
|
+
|
|
66
|
+
`Guardrails.configuration.visual_diff.adapter` names a registered adapter. snap_diff is built in; other gems register theirs:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Guardrails::VisualDiff.register_adapter(:browser) do |root:, config:|
|
|
70
|
+
MyAdapter.new(root: root) # #collect returns Guardrails::VisualDiff::Finding objects
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`config` is `Guardrails.configuration.visual_diff`. The name is checked when the adapter is built, so a host initializer can set `adapter = :browser` before the gem that registers it loads. An unregistered name fails with the registered ones listed.
|
|
75
|
+
|
|
76
|
+
Besides the fields below, adapter findings can set `status` (`"changed"`, `"new"`, ...), `comment`, `regions` (`[{ x:, y:, w:, h: }]`) and `extra_details` (`[[label, value], ...]`). They show up as report details, and a finding with a `status` but no ratio is titled `[changed] scenario`. As with snap_diff, a finding with `mismatch_ratio: nil` always fails.
|
|
77
|
+
|
|
78
|
+
[ui_guardrails-browser](https://github.com/meticulous/guardrails-browser) registers `:browser`, which reports unresolved items from its own capture and review loop.
|
|
79
|
+
|
|
64
80
|
## How the snap_diff adapter works
|
|
65
81
|
|
|
66
82
|
The snap_diff-capybara gem commits baseline screenshots to git under `doc/screenshots/<name>.png` and, on test failure, writes a sibling `<name>.diff.png` (changed pixels in red) plus a `<name>.heatmap.diff.png` (variance density). Tests fail on baseline mismatches in CI, so the diff files are an artifact of "this regression slipped through to the artifact directory."
|
|
@@ -44,8 +44,9 @@ module Guardrails
|
|
|
44
44
|
DEFAULT_ADAPTER = :snap_diff
|
|
45
45
|
DEFAULT_SNAP_DIFF_DIR = "doc/screenshots"
|
|
46
46
|
DEFAULT_THRESHOLD = 0.0 # any non-zero mismatch fails
|
|
47
|
-
KNOWN_ADAPTERS = %i[snap_diff backstop].freeze
|
|
48
47
|
|
|
48
|
+
# `backstop_json` is reserved for the BackstopJS adapter and unused
|
|
49
|
+
# until it lands; kept so initializers that already set it still load.
|
|
49
50
|
attr_accessor :enabled, :adapter, :snap_diff_dir, :backstop_json, :threshold
|
|
50
51
|
|
|
51
52
|
def initialize
|
|
@@ -56,16 +57,14 @@ module Guardrails
|
|
|
56
57
|
@threshold = DEFAULT_THRESHOLD
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
# Whether the name is a registered adapter is checked when the
|
|
61
|
+
# adapter is built (VisualDiff#adapter), not here: an adapter from
|
|
62
|
+
# another gem may register after this initializer runs.
|
|
59
63
|
def adapter=(value)
|
|
60
64
|
raise ArgumentError, "visual_diff.adapter cannot be nil" if value.nil?
|
|
61
65
|
raise ArgumentError, "visual_diff.adapter cannot be blank" if value.respond_to?(:strip) && value.strip.empty?
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
unless KNOWN_ADAPTERS.include?(sym)
|
|
65
|
-
raise ArgumentError,
|
|
66
|
-
"Unknown visual_diff adapter #{value.inspect}; expected one of #{KNOWN_ADAPTERS.inspect}"
|
|
67
|
-
end
|
|
68
|
-
@adapter = sym
|
|
67
|
+
@adapter = value.to_sym
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
def threshold=(value)
|
|
@@ -43,9 +43,19 @@ module Guardrails
|
|
|
43
43
|
{
|
|
44
44
|
category: category, severity: severity, title: title, suggestion: suggestion,
|
|
45
45
|
locations: locations.map(&:to_h), snippet: snippet,
|
|
46
|
-
details:
|
|
46
|
+
details: details_hash, auto_fix: auto_fix || false
|
|
47
47
|
}
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
# `details` allows a label more than once (two screenshots, several
|
|
51
|
+
# regions). A plain `to_h` kept only the last value; a repeated
|
|
52
|
+
# label collects its values into an array instead. Labels that
|
|
53
|
+
# appear once stay scalars, so existing JSON doesn't change.
|
|
54
|
+
def details_hash
|
|
55
|
+
details.each_with_object({}) do |(label, value), hash|
|
|
56
|
+
hash[label] = hash.key?(label) ? Array(hash[label]) << value : value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
# A named group of findings plus the framing paragraph that
|
data/lib/guardrails/version.rb
CHANGED
|
@@ -14,7 +14,9 @@ module Guardrails
|
|
|
14
14
|
#
|
|
15
15
|
# Adapter selection comes from `Guardrails.configuration.visual_diff.adapter`
|
|
16
16
|
# (default `:snap_diff`, the Rails-native baselines-in-git workflow).
|
|
17
|
-
#
|
|
17
|
+
# Other gems add adapters with `VisualDiff.register_adapter`; the
|
|
18
|
+
# ui_guardrails-browser gem registers `:browser`. BackstopJS support
|
|
19
|
+
# tracked in issue #15.
|
|
18
20
|
class VisualDiff
|
|
19
21
|
# Normalized across every adapter. Fields adapter-specific shapes
|
|
20
22
|
# don't supply are nil — consumers should not assume presence.
|
|
@@ -30,6 +32,10 @@ module Guardrails
|
|
|
30
32
|
:diff_path, # relative path to the diff image, nil when no diff exists
|
|
31
33
|
:url, # optional — set by adapters whose scenarios carry URLs (BackstopJS)
|
|
32
34
|
:selector, # optional — same
|
|
35
|
+
:status, # optional — adapter's review state, e.g. "changed", "new", "rejected"
|
|
36
|
+
:comment, # optional — a reviewer's note on the change
|
|
37
|
+
:regions, # optional — changed areas, [{ x:, y:, w:, h: }, ...]
|
|
38
|
+
:extra_details, # optional — more [label, value] pairs for the report
|
|
33
39
|
keyword_init: true
|
|
34
40
|
) do
|
|
35
41
|
def to_h
|
|
@@ -37,6 +43,34 @@ module Guardrails
|
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
@adapters = {}
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Adds an adapter under `name`. The block builds it:
|
|
50
|
+
#
|
|
51
|
+
# Guardrails::VisualDiff.register_adapter(:browser) do |root:, config:|
|
|
52
|
+
# MyAdapter.new(root: root)
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# `config` is `Guardrails.configuration.visual_diff`. The adapter's
|
|
56
|
+
# `#collect` returns `VisualDiff::Finding`s; a finding with
|
|
57
|
+
# `mismatch_ratio: nil` always fails, one with a ratio fails above
|
|
58
|
+
# the threshold.
|
|
59
|
+
def register_adapter(name, &factory)
|
|
60
|
+
raise ArgumentError, "register_adapter needs a block that builds the adapter" unless factory
|
|
61
|
+
|
|
62
|
+
@adapters[name.to_sym] = factory
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def adapters
|
|
66
|
+
@adapters.keys
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def adapter_factory(name)
|
|
70
|
+
@adapters[name]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
40
74
|
def initialize(root:, output: $stdout,
|
|
41
75
|
adapter: nil, threshold: nil, style: nil)
|
|
42
76
|
@root = Pathname(root)
|
|
@@ -82,8 +116,7 @@ module Guardrails
|
|
|
82
116
|
title: "#{ratio_label(f)} #{f.scenario}#{f.viewport ? " (#{f.viewport})" : ""}",
|
|
83
117
|
suggestion: SUGGESTION,
|
|
84
118
|
locations: [f.diff_path, f.baseline_path].compact.map { |path| Report::Location.new(file: path) },
|
|
85
|
-
details:
|
|
86
|
-
.reject { |_, v| v.nil? }
|
|
119
|
+
details: details_for(f)
|
|
87
120
|
)
|
|
88
121
|
}
|
|
89
122
|
)]
|
|
@@ -100,19 +133,27 @@ module Guardrails
|
|
|
100
133
|
SUGGESTION = "compare baseline ↔ diff; accept the new baseline or fix the regression"
|
|
101
134
|
|
|
102
135
|
def ratio_label(finding)
|
|
103
|
-
|
|
136
|
+
return "[#{(finding.mismatch_ratio * 100).round(2)}% mismatch]" unless finding.mismatch_ratio.nil?
|
|
137
|
+
|
|
138
|
+
finding.status ? "[#{finding.status}]" : "[diff present]"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def details_for(finding)
|
|
142
|
+
regions = finding.regions&.map { |r| "#{r[:x] || r["x"]},#{r[:y] || r["y"]} #{r[:w] || r["w"]}x#{r[:h] || r["h"]}" }
|
|
143
|
+
[["baseline", finding.baseline_path], ["current", finding.current_path], ["diff", finding.diff_path],
|
|
144
|
+
["url", finding.url], ["selector", finding.selector], ["status", finding.status],
|
|
145
|
+
["comment", finding.comment], ["regions", regions&.join("; ")]]
|
|
146
|
+
.reject { |_, v| v.nil? || v == "" } + Array(finding.extra_details)
|
|
104
147
|
end
|
|
105
148
|
|
|
106
149
|
def adapter
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
dir: Guardrails.configuration.visual_diff.snap_diff_dir
|
|
112
|
-
)
|
|
113
|
-
else
|
|
114
|
-
raise ArgumentError, "Unknown visual_diff adapter: #{@adapter_name.inspect}"
|
|
150
|
+
factory = self.class.adapter_factory(@adapter_name)
|
|
151
|
+
unless factory
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"Unknown visual_diff adapter: #{@adapter_name.inspect}; registered: #{self.class.adapters.inspect}"
|
|
115
154
|
end
|
|
155
|
+
|
|
156
|
+
factory.call(root: @root, config: Guardrails.configuration.visual_diff)
|
|
116
157
|
end
|
|
117
158
|
|
|
118
159
|
def coerce_adapter(value)
|
|
@@ -149,9 +190,14 @@ module Guardrails
|
|
|
149
190
|
@output.puts " #{@style.location("diff: #{f.diff_path}")}" if f.diff_path
|
|
150
191
|
@output.puts " #{@style.location("url: #{f.url}")}" if f.url
|
|
151
192
|
@output.puts " #{@style.location("selector: #{f.selector}")}" if f.selector
|
|
193
|
+
@output.puts " #{@style.location("comment: #{f.comment}")}" if f.comment
|
|
152
194
|
end
|
|
153
195
|
end
|
|
154
196
|
end
|
|
155
197
|
end
|
|
156
198
|
|
|
157
199
|
require_relative "visual_diff/snap_diff"
|
|
200
|
+
|
|
201
|
+
Guardrails::VisualDiff.register_adapter(:snap_diff) do |root:, config:|
|
|
202
|
+
Guardrails::VisualDiff::SnapDiff.new(root: root, dir: config.snap_diff_dir)
|
|
203
|
+
end
|