capybara-screenshot-diff 1.14.0 → 1.15.1

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: dba172cde687794eeedbffe4c4112d58ed5c4e2fcac5cdaf93b84827396e832d
4
- data.tar.gz: a6b45b9bfc4ff349b506c7045f54392101c1bbfecda7b51475758fd2ee1536c0
3
+ metadata.gz: 41c48f66867ecb07078242d441547616e4b8a57b722c34801bf575e9659cc207
4
+ data.tar.gz: cdbf947ae46b6848700ffb7be78ebb744fbe9514f442e242da1bcd5f9acf15b8
5
5
  SHA512:
6
- metadata.gz: afc332553b0bdafe8a47d82376b83b866bcd25144132f6e6ba481e41b03fd113ea74cf25f1decfcff8397664b1d4e2469abbfd21bebfde2a2c42f332ca0220dc
7
- data.tar.gz: 1bd843fa71d17a0eac95bf1674a8d76352a8fe8c93c581840f9a50f63106edd40c5964b25ac36add805c6baf4129138e01aca5f73df30b34646fb1c2814adacd
6
+ metadata.gz: 1660ab2b09f17cbc98eb92a144aee6278743a4bb19302f7386959ba2f634679344991098ece65a8300fcca6e4116f5b8f72cfe77e147b453269c522a7aa71651
7
+ data.tar.gz: 982e52856c449f1916688bfbd44ca832c93a12d25ba809e4068c61492d9163a85b84898fc60ad6c450c299bc1b70839a3eed400d334034b2cefd98ff47bb137f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v1.15.1] - 2026-08-22
9
+
10
+ ### Fixed
11
+ - **`VipsDriver#resize_image_to` resizes to the requested dimensions** — the vips driver passed a target aspect ratio where libvips expects a scale factor, so retina-halving on macOS/Selenium could enlarge screenshots (2560×1600 → 4096×2560) and save wrongly-sized baselines; now scales width and height independently via `resize(scale, vscale:)` ([#205](https://github.com/snap-diff/snap_diff-capybara/pull/205))
12
+
13
+ ### Internal
14
+ - Driver contract tests pinning the shared ChunkyPNG/Vips driver seam (method surface, load/compare behavior, option handling) — the net that caught the resize bug ([#204](https://github.com/snap-diff/snap_diff-capybara/pull/204))
15
+
16
+ ---
17
+
18
+ ## [v1.15.0] - 2026-08-22
19
+
20
+ ### Added
21
+ - **`SnapDiff::Config`** — one flat object consolidating all 27 settings behind `SnapDiff.config` / `SnapDiff.configure { |config| ... }`; purely additive, the existing accessors stay canonical and both views share one source of truth ([#200](https://github.com/snap-diff/snap_diff-capybara/pull/200))
22
+
23
+ ### Fixed
24
+ - **`pending_if_new` no longer masks real failures** — a raising Minitest `teardown` or a raising RSpec `after` hook registered before the gem could previously be reported as a skipped/pending test with exit 0; the pending marker now defers until the framework's teardown chain has run and yields to any failure. Known residual: an RSpec `config.append_after` registered after this gem still runs later than the gem's hook — require the gem last if you use appended after-hooks ([#203](https://github.com/snap-diff/snap_diff-capybara/pull/203))
25
+ - **`BacktraceFilter` path-boundary matching** — a custom `lib_directory` of `/app/lib` no longer also filters locations under `/app/library` ([#202](https://github.com/snap-diff/snap_diff-capybara/pull/202))
26
+
27
+ ### Internal
28
+ - Failure-masking guard tests for the `pending_if_new` teardown paths via subprocess RSpec runs ([#199](https://github.com/snap-diff/snap_diff-capybara/pull/199))
29
+ - Hygiene pass — skip-area/VCS-baseline and `BacktraceFilter` guard tests, entry-point forwarder dedupe, `Comparison` and `BacktraceFilter` merged into their sole consumers (−2 files, constant paths preserved) ([#201](https://github.com/snap-diff/snap_diff-capybara/pull/201))
30
+
31
+ ---
32
+
8
33
  ## [v1.14.0] - 2026-08-22
9
34
 
10
35
  ### Added
data/docs/RELEASE_PREP.md CHANGED
@@ -1,23 +1,23 @@
1
- # Release Preparation — v1.14.0
1
+ # Release Preparation — v1.15.1
2
2
 
3
3
  ## Summary
4
4
 
5
- SnapDiff namespace aliases (ADR-004 PR 9), standalone-require fix, and three
6
- internal consolidation refactors (ADR-004 PR 2/PR 7, teardown logic).
5
+ HIGH-severity VipsDriver resize fix (retina halving could enlarge screenshots
6
+ and corrupt baselines) plus the driver contract tests that caught it.
7
7
 
8
8
  ## Release Checklist
9
9
 
10
10
  ### Pre-Release
11
11
 
12
- - [x] Update version to `1.14.0`
13
- - [x] Run tests: `bundle exec rake test:unit` (green)
12
+ - [x] Update version to `1.15.1`
13
+ - [x] Run tests: `bundle exec rake test:unit` (376 runs, 0 failures)
14
14
  - [x] Update CHANGELOG.md
15
15
 
16
16
  ### Release (One Click)
17
17
 
18
18
  1. Push to GitHub
19
19
  2. Go to [Actions → Release](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/release.yml)
20
- 3. Click **Run workflow**, enter `1.14.0`
20
+ 3. Click **Run workflow**, enter `1.15.1`
21
21
  4. Workflow will: test → tag → publish to RubyGems → create GitHub Release
22
22
 
23
23
  ### Post-Release
@@ -28,17 +28,17 @@ internal consolidation refactors (ADR-004 PR 2/PR 7, teardown logic).
28
28
  ## What Changed
29
29
 
30
30
  ### Added
31
- - `SnapDiff` namespace aliases `SnapDiff::Comparison`, `SnapDiff.compare`,
32
- `SnapDiff.start` (additive; deprecations deferred to v2.0)
31
+ - `SnapDiff::Config` flat, additive consolidation of all 27 settings
32
+ (`SnapDiff.config` / `SnapDiff.configure`); old accessors stay canonical
33
33
 
34
34
  ### Fixed
35
- - Standalone `require "capybara_screenshot_diff"` + `Diff.compare` no longer
36
- raises `NameError` (missing drivers require)
35
+ - `pending_if_new` no longer converts real teardown/after-hook failures into
36
+ pending tests (Minitest defers to `after_teardown`; RSpec uses `append_after`;
37
+ known residual for consumer `append_after` hooks documented)
38
+ - `BacktraceFilter` custom `lib_directory` matches on a path boundary
37
39
 
38
40
  ### Internal
39
- - `DifferenceFinder` merged into `ImageCompare`; `AnnotationService` extracted
40
- from `Reporters::Default` (public surface preserved); `pending_if_new`
41
- teardown consolidated across the three framework adapters; driver-coverage
42
- banner + CI guard in the test suite
41
+ - Guard tests for failure masking and skip-area/VCS-baseline regressions;
42
+ two files merged into sole consumers (constant paths preserved)
43
43
 
44
44
  See [CHANGELOG.md](../CHANGELOG.md) for full details.
data/docs/architecture.md CHANGED
@@ -239,7 +239,6 @@ lib/
239
239
  screenshot_assertion.rb # Assertion + registry objects
240
240
  attempts_reporter.rb # Debug reporting for unstable captures
241
241
  error_with_filtered_backtrace.rb # Error with filtered stack
242
- backtrace_filter.rb # Library/gem frame filtering
243
242
  reporters/
244
243
  html.rb # Interactive HTML report reporter
245
244
  templates/report.html.erb # HTML report template
@@ -255,7 +254,6 @@ lib/
255
254
  stable_screenshoter.rb # Stability detection wrapper
256
255
  screenshot_matcher.rb # Orchestrator for capture + compare
257
256
  image_compare.rb # Layered comparison engine
258
- comparison.rb # Comparison value object
259
257
  difference.rb # Difference result value object
260
258
  image_preprocessor.rb # Pre-processing (skip areas, median filter)
261
259
  area_calculator.rb # Crop/skip area coordinate resolution
@@ -76,7 +76,7 @@ module Capybara
76
76
  end
77
77
 
78
78
  def resize_image_to(image, new_width, new_height)
79
- image.resize(new_width.to_f / new_height)
79
+ image.resize(new_width.to_f / image.width, vscale: new_height.to_f / image.height)
80
80
  end
81
81
 
82
82
  def load_images(old_file_name, new_file_name)
@@ -3,7 +3,6 @@
3
3
  require "pathname"
4
4
  require "fileutils"
5
5
 
6
- require "capybara/screenshot/diff/comparison"
7
6
  require "capybara/screenshot/diff/difference"
8
7
  require "capybara/screenshot/diff/drivers"
9
8
  require "capybara/screenshot/diff/image_preprocessor"
@@ -14,6 +13,13 @@ module Capybara
14
13
  module Diff
15
14
  LOADED_DRIVERS = {}
16
15
 
16
+ # Holds the two images (and their paths/options/driver) being compared.
17
+ class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
18
+ def skip_area
19
+ options[:skip_area]
20
+ end
21
+ end
22
+
17
23
  # Handles comparison of two images with a focus on performance and accuracy.
18
24
  #
19
25
  # This class implements a multi-layered optimization strategy for image comparison:
@@ -3,7 +3,7 @@
3
3
  module Capybara
4
4
  module Screenshot
5
5
  module Diff
6
- VERSION = "1.14.0"
6
+ VERSION = "1.15.1"
7
7
  end
8
8
  end
9
9
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara_screenshot_diff/minitest"
3
+ require "capybara-screenshot-diff"
@@ -1,8 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara_screenshot_diff/backtrace_filter"
4
-
5
3
  module CapybaraScreenshotDiff
4
+ class BacktraceFilter
5
+ LIB_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), "..")) + File::SEPARATOR
6
+
7
+ def initialize(lib_directory = LIB_DIRECTORY)
8
+ # Trailing separator keeps the prefix match on a directory boundary,
9
+ # so "/app/lib" does not also reject "/app/library".
10
+ @lib_directory = lib_directory.end_with?(File::SEPARATOR) ? lib_directory : lib_directory + File::SEPARATOR
11
+ end
12
+
13
+ # Filters out any backtrace lines originating from the library directory or from gems such as ActiveSupport, Minitest, and Railties
14
+ # @param backtrace [Array<String>]
15
+ # @return [Array<String>]
16
+ def filtered(backtrace)
17
+ backtrace
18
+ .reject { |location| File.expand_path(location).start_with?(@lib_directory) }
19
+ .reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
20
+ end
21
+ end
22
+
6
23
  # @private
7
24
  class ErrorWithFilteredBacktrace < StandardError
8
25
  # @private
@@ -36,9 +36,10 @@ module CapybaraScreenshotDiff
36
36
  super
37
37
  CapybaraScreenshotDiff.verify
38
38
 
39
- if (msg = CapybaraScreenshotDiff.pending_screenshots_message)
40
- skip(msg)
41
- end
39
+ # Computed here (before teardown/reset), but the actual `skip` is
40
+ # deferred to `after_teardown` so a real error raised by the user's
41
+ # `teardown` isn't masked by a pending skip recorded before it ran.
42
+ @capybara_screenshot_diff_pending_message = CapybaraScreenshotDiff.pending_screenshots_message
42
43
  rescue CapybaraScreenshotDiff::ExpectationNotMet => e
43
44
  assertion = ::Minitest::Assertion.new(e)
44
45
  assertion.set_backtrace(e.backtrace)
@@ -46,6 +47,16 @@ module CapybaraScreenshotDiff
46
47
  ensure
47
48
  CapybaraScreenshotDiff.reset
48
49
  end
50
+
51
+ def after_teardown
52
+ super
53
+
54
+ # Never mask a real failure (from `verify` above or from the user's
55
+ # own `teardown`) with a pending marker.
56
+ if failures.empty? && (msg = @capybara_screenshot_diff_pending_message)
57
+ skip(msg)
58
+ end
59
+ end
49
60
  end
50
61
  end
51
62
  end
@@ -30,12 +30,27 @@ RSpec.configure do |config|
30
30
  end
31
31
  end
32
32
 
33
- config.after do |example|
33
+ # `append_after` (as opposed to the default `after`, which prepends) adds
34
+ # this hook to the *end* of the after-hook chain regardless of when it's
35
+ # registered relative to the user's own `after`/`config.after` hooks. RSpec
36
+ # runs `after(:each)` hooks in reverse registration order, so a plain
37
+ # `config.after` here would run BEFORE a user hook registered earlier in
38
+ # their own spec_helper (before this file was required) -- and if that
39
+ # later-running user hook raises, its exception gets folded into
40
+ # `pending_exception` instead of `example.exception`, silently masking the
41
+ # failure behind our pending skip. `append_after` runs after the full user
42
+ # after-chain no matter the registration order, closing that gap.
43
+ config.append_after do |example|
34
44
  if self.class.include?(CapybaraScreenshotDiff::DSL)
35
45
  begin
36
46
  CapybaraScreenshotDiff.verify
37
47
 
38
- # Never mask a real failure with a pending marker.
48
+ # Never mask a real failure with a pending marker. Kept as
49
+ # defense-in-depth: `append_after` observes failures from plain
50
+ # `after`/`prepend_after` user hooks, but appended hooks run FIFO,
51
+ # so a user `append_after` registered after this gem still runs
52
+ # later than us — RSpec has no "run absolutely last" construct.
53
+ # Mitigation for such consumers: require this gem last.
39
54
  if example.exception.nil? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
40
55
  skip(msg)
41
56
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MAPPING below references the legacy modules, so they must be loaded first.
4
+ require "capybara_screenshot_diff"
5
+
6
+ module SnapDiff
7
+ # Flat, additive consolidation of every existing
8
+ # +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ +mattr_accessor+
9
+ # setting behind one object: <tt>SnapDiff.config.<em>attr</em></tt>.
10
+ #
11
+ # Storage ownership: the OLD mattr_accessors remain the single source of
12
+ # truth. Config holds no state of its own -- every reader/writer defined
13
+ # from {MAPPING} simply forwards to the existing accessor. This is
14
+ # deliberate, not just simplest: several of those accessors carry default
15
+ # logic with observable timing (+fail_if_new+ derives its default from
16
+ # +ENV["CI"]+, +root+ falls back to +Rails.root+) that Rails' +mattr_accessor+
17
+ # evaluates once, at class-body-eval time, when +capybara_screenshot_diff.rb+
18
+ # first loads. Re-implementing that logic here -- even faithfully -- would
19
+ # mean evaluating it again, at a different moment (Config#new time), which
20
+ # is exactly the kind of "when defaults are evaluated" divergence the v2
21
+ # consolidation must not introduce. Delegating sidesteps the question
22
+ # entirely: Config never evaluates a default, it only ever forwards to
23
+ # whichever accessor already owns one. Bidirectional consistency (a write
24
+ # through either the old accessor or Config is visible through the other)
25
+ # falls out for free, because both paths read and write the exact same
26
+ # class-variable-backed storage.
27
+ class Config
28
+ # config attr name => [owning module, mattr_accessor name].
29
+ #
30
+ # The two names differ only for +screenshot_enabled+:
31
+ # +Capybara::Screenshot.enabled+ and +Capybara::Screenshot::Diff.enabled+
32
+ # are independent settings (see +Capybara::Screenshot.active?+, which
33
+ # reads both) that happen to share a bare name in their own modules. A
34
+ # flat Config can't expose two attributes both called +enabled+, so the
35
+ # Screenshot-side one is renamed here; Diff's keeps the bare +enabled+
36
+ # name since it's the one most existing configuration touches directly.
37
+ MAPPING = {
38
+ # Capybara::Screenshot
39
+ add_driver_path: [Capybara::Screenshot, :add_driver_path],
40
+ add_os_path: [Capybara::Screenshot, :add_os_path],
41
+ blur_active_element: [Capybara::Screenshot, :blur_active_element],
42
+ screenshot_enabled: [Capybara::Screenshot, :enabled],
43
+ hide_caret: [Capybara::Screenshot, :hide_caret],
44
+ disable_animations: [Capybara::Screenshot, :disable_animations],
45
+ root: [Capybara::Screenshot, :root],
46
+ stability_time_limit: [Capybara::Screenshot, :stability_time_limit],
47
+ window_size: [Capybara::Screenshot, :window_size],
48
+ save_path: [Capybara::Screenshot, :save_path],
49
+ use_lfs: [Capybara::Screenshot, :use_lfs],
50
+ screenshot_format: [Capybara::Screenshot, :screenshot_format],
51
+ capybara_screenshot_options: [Capybara::Screenshot, :capybara_screenshot_options],
52
+ # Capybara::Screenshot::Diff
53
+ delayed: [Capybara::Screenshot::Diff, :delayed],
54
+ area_size_limit: [Capybara::Screenshot::Diff, :area_size_limit],
55
+ fail_if_new: [Capybara::Screenshot::Diff, :fail_if_new],
56
+ pending_if_new: [Capybara::Screenshot::Diff, :pending_if_new],
57
+ fail_on_difference: [Capybara::Screenshot::Diff, :fail_on_difference],
58
+ color_distance_limit: [Capybara::Screenshot::Diff, :color_distance_limit],
59
+ enabled: [Capybara::Screenshot::Diff, :enabled],
60
+ shift_distance_limit: [Capybara::Screenshot::Diff, :shift_distance_limit],
61
+ skip_area: [Capybara::Screenshot::Diff, :skip_area],
62
+ driver: [Capybara::Screenshot::Diff, :driver],
63
+ tolerance: [Capybara::Screenshot::Diff, :tolerance],
64
+ perceptual_threshold: [Capybara::Screenshot::Diff, :perceptual_threshold],
65
+ screenshoter: [Capybara::Screenshot::Diff, :screenshoter],
66
+ manager: [Capybara::Screenshot::Diff, :manager]
67
+ }.freeze
68
+
69
+ MAPPING.each do |name, (mod, mattr)|
70
+ define_method(name) { mod.public_send(mattr) }
71
+ define_method(:"#{name}=") { |value| mod.public_send(:"#{mattr}=", value) }
72
+ end
73
+ end
74
+ end
data/lib/snap_diff.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "capybara_screenshot_diff"
4
+ require "snap_diff/config"
4
5
 
5
6
  # Forward-looking namespace for the gem, per ADR-004.
6
7
  #
@@ -14,7 +15,35 @@ module SnapDiff
14
15
  Capybara::Screenshot::Diff.compare(...)
15
16
  end
16
17
 
18
+ # v1-style configuration: yields the two existing mattr_accessor holders
19
+ # (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+) exactly as
20
+ # +Capybara::Screenshot::Diff.configure+ always has. Kept byte-for-byte
21
+ # identical to +Diff.configure+ for existing callers migrating namespaces
22
+ # without changing call shape.
23
+ #
24
+ # SnapDiff.start do |screenshot, diff|
25
+ # screenshot.window_size = [1280, 1024]
26
+ # diff.tolerance = 0.0005
27
+ # end
17
28
  def self.start(&block)
18
29
  Capybara::Screenshot::Diff.configure(&block)
19
30
  end
31
+
32
+ # Forward-looking configuration: yields the single consolidated
33
+ # {SnapDiff::Config} object instead of the two old holders. Same
34
+ # underlying storage as +start+ / the old mattr_accessors -- this is a
35
+ # different *shape* of the same settings, not a second source of truth.
36
+ #
37
+ # SnapDiff.configure do |config|
38
+ # config.window_size = [1280, 1024]
39
+ # config.tolerance = 0.0005
40
+ # end
41
+ def self.configure
42
+ yield config
43
+ end
44
+
45
+ # The single consolidated settings object. See {SnapDiff::Config}.
46
+ def self.config
47
+ @config ||= Config.new
48
+ end
20
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-screenshot-diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.0
4
+ version: 1.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uwe Kubosch
@@ -101,7 +101,6 @@ files:
101
101
  - lib/capybara/screenshot/diff/annotation_service.rb
102
102
  - lib/capybara/screenshot/diff/area_calculator.rb
103
103
  - lib/capybara/screenshot/diff/browser_helpers.rb
104
- - lib/capybara/screenshot/diff/comparison.rb
105
104
  - lib/capybara/screenshot/diff/cucumber.rb
106
105
  - lib/capybara/screenshot/diff/difference.rb
107
106
  - lib/capybara/screenshot/diff/drivers.rb
@@ -121,7 +120,6 @@ files:
121
120
  - lib/capybara/screenshot/diff/version.rb
122
121
  - lib/capybara_screenshot_diff.rb
123
122
  - lib/capybara_screenshot_diff/attempts_reporter.rb
124
- - lib/capybara_screenshot_diff/backtrace_filter.rb
125
123
  - lib/capybara_screenshot_diff/cucumber.rb
126
124
  - lib/capybara_screenshot_diff/dsl.rb
127
125
  - lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb
@@ -135,6 +133,7 @@ files:
135
133
  - lib/capybara_screenshot_diff/snap_manager.rb
136
134
  - lib/capybara_screenshot_diff/static.rb
137
135
  - lib/snap_diff.rb
136
+ - lib/snap_diff/config.rb
138
137
  homepage: https://github.com/snap-diff/snap_diff-capybara
139
138
  licenses:
140
139
  - MIT
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Capybara::Screenshot::Diff
4
- class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
5
- def skip_area
6
- options[:skip_area]
7
- end
8
- end
9
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CapybaraScreenshotDiff
4
- class BacktraceFilter
5
- LIB_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), "..")) + File::SEPARATOR
6
-
7
- def initialize(lib_directory = LIB_DIRECTORY)
8
- @lib_directory = lib_directory
9
- end
10
-
11
- # Filters out any backtrace lines originating from the library directory or from gems such as ActiveSupport, Minitest, and Railties
12
- # @param backtrace [Array<String>]
13
- # @return [Array<String>]
14
- def filtered(backtrace)
15
- backtrace
16
- .reject { |location| File.expand_path(location).start_with?(@lib_directory) }
17
- .reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
18
- end
19
- end
20
- end