Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions capybara-screenshot-diff.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "capybara/screenshot/diff/version"
require "snap_diff/version"

Gem::Specification.new do |spec|
spec.name = "capybara-screenshot-diff"
spec.version = Capybara::Screenshot::Diff::VERSION
spec.version = SnapDiff::VERSION
spec.authors = ["Uwe Kubosch"]
spec.email = ["uwe@kubosch.no"]
spec.summary = "Track your GUI changes with diff assertions"
Expand Down
13 changes: 13 additions & 0 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ Two deliberate consequences of the lazy shim design — both flagged for feedbac

2. **Reopening `module Capybara::Screenshot::Diff::Drivers` shadows the shim.** The historical custom-driver monkey-patch pattern defines a fresh, empty `Drivers` module instead of reaching the real one. Define custom drivers under `SnapDiff::Drivers` instead — and note `BaseDriver` is gone as a superclass: `class MyDriver < BaseDriver` becomes `include SnapDiff::Driver` (it's a mixin now).

#### Two moves that fail *silently* if you miss them

**Stubbing the detected-drivers list.** The value moved to `SnapDiff::Drivers::AVAILABLE_DRIVERS`, and `Capybara::Screenshot::Diff::AVAILABLE_DRIVERS` is now an eager alias of it. *Reading* either is identical, but **stubbing the legacy name only rebinds the alias** — the gem keeps reading the canonical constant, so a test that stubs it to `[]` no longer exercises the no-drivers path and just passes for the wrong reason:

```ruby
# before
Capybara::Screenshot::Diff.stub_const(:AVAILABLE_DRIVERS, []) { ... }
# now
SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) { ... }
```

**`SnapDiff::Config::MAPPING` is gone.** It split in two: `SnapDiff::Config::SETTINGS` (the setting names, no legacy knowledge) and `SnapDiff::LegacyShims::CONFIG_MAPPING` (which legacy holder each name hangs off). If you referenced `MAPPING` — iterating settings in a test helper, say — use `SETTINGS`; `CONFIG_MAPPING` is `@api private` and disappears in 3.0 with the rest of the v1 surface.

---

### FAQ
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ Drivers abstract image processing operations. Shared default behavior lives in t
| `merge` | Composite images | Not applicable |
| `highlight_mask` | Conditional color overlay | Not applicable |

**Auto-detection:** `Utils.detect_available_drivers` tries to load `:vips` first (via `ruby-vips` gem), then `:chunky_png`. The `:auto` driver mode picks the first available.
**Auto-detection:** `SnapDiff::Drivers.detect_available` tries to load `:vips` first (via `ruby-vips` gem), then `:chunky_png`. The `:auto` driver mode picks the first available. `Utils.detect_available_drivers` is the older name and one-lines into it.

**Registry (ADR-008 step 5b):** `SnapDiff::Drivers.loaded` is the canonical driver-class cache — a `name => class` hash filled lazily by `Utils.find_driver_class_for`, and the registration point for custom drivers (the legacy `Capybara::Screenshot::Diff::LOADED_DRIVERS` is an eager same-object alias, so registrations through either land in the same hash). `SnapDiff::Drivers.available` is the canonical read API for the detected list; the value itself still lives on `Capybara::Screenshot::Diff::AVAILABLE_DRIVERS`, which stays the published stubbing point. `SnapDiff::Drivers.for` resolves an options hash to a driver instance. See [Custom drivers](snapdiff.md#custom-drivers).
**Registry (ADR-008 step 5b):** `SnapDiff::Drivers.loaded` is the canonical driver-class cache — a `name => class` hash filled lazily by `Utils.find_driver_class_for`, and the registration point for custom drivers (the legacy `Capybara::Screenshot::Diff::LOADED_DRIVERS` is an eager same-object alias, so registrations through either land in the same hash). `SnapDiff::Drivers.available` is the canonical read API for the detected list, and since the 3.0-readiness pass the value lives with it, as `SnapDiff::Drivers::AVAILABLE_DRIVERS` — that constant is now the published stubbing point, and the legacy `Capybara::Screenshot::Diff::AVAILABLE_DRIVERS` is an eager same-object alias of it. `SnapDiff::Drivers.for` resolves an options hash to a driver instance. See [Custom drivers](snapdiff.md#custom-drivers).

### 6. Difference Region Detection

Expand Down Expand Up @@ -221,7 +221,9 @@ Test begins

Since ADR-008 step 1 the storage ownership is inverted from the original v2 consolidation: **`SnapDiff::Config` (`lib/snap_diff/config.rb`) IS the storage** — one eagerly-created instance, reachable as `SnapDiff.config`, holding every setting as a plain `attr_accessor`. It is the leaf of the config require graph and requires nothing that leads back to either entry point.

The legacy `Capybara::Screenshot.*` / `Capybara::Screenshot::Diff.*` accessors are thin delegators generated from `Config::MAPPING` (both singleton and instance methods, matching what `mattr_accessor` used to define) that forward to that one object. One storage, two views — a write through either surface is visible through the other structurally, not by synchronization. `lib/capybara/screenshot/diff/config_legacy.rb` remains at the old path, but it now installs the delegating surface rather than owning the state.
The legacy `Capybara::Screenshot.*` / `Capybara::Screenshot::Diff.*` accessors are thin delegators generated from `SnapDiff::LegacyShims::CONFIG_MAPPING` (both singleton and instance methods, matching what `mattr_accessor` used to define) that forward to that one object. One storage, two views — a write through either surface is visible through the other structurally, not by synchronization.

Since the 3.0-readiness pass, `lib/snap_diff/legacy_shims.rb` is the single file that holds the v1 surface as code: the `const_missing` forwarders, `CONFIG_MAPPING` and its generator, the derived forwarders (`Screenshot.active?`, `Diff.configure`, `Diff.default_options`, …) and `SnapDiff.start`. `Config` itself names nothing from the v1 namespaces — it declares its settings in `Config::SETTINGS`, and `LegacyShims::CONFIG_MAPPING` says which legacy holder each one is exposed on (an invariant pinned by `snap_diff_config_test.rb`). `lib/capybara/screenshot/diff/config_legacy.rb` remains at the old path as a pair of requires.

The two legacy views are organized into two namespaces:

Expand Down
70 changes: 12 additions & 58 deletions lib/capybara/screenshot/diff/config_legacy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,20 @@

# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff config surface.
#
# Since ADR-008 step 1 the storage lives in SnapDiff::Config -- the require
# leaf of the config graph (see its own header) -- and since step 7b the
# DERIVED values (active?, screenshot_area, default_options) live there
# too. snap_diff/config.rb also generates the old accessor names as thin
# delegators from SnapDiff::Config::MAPPING, so nothing but forwarders is
# left here. The v1 surface (Capybara::Screenshot.window_size = ...,
# Diff.configure { ... }, Diff.compare) keeps working unchanged: one
# storage, two views.
# Nothing but requires is left here. The storage is SnapDiff::Config
# (ADR-008 step 1, the require leaf of the config graph); the derived values
# (active?, screenshot_area, default_options) live there too since step 7b;
# and the old accessor names, Diff.configure/.compare, SnapDiff.start and
# the AVAILABLE_DRIVERS alias are generated by snap_diff/legacy_shims -- the
# one file that holds the v1 surface as code, so that the canonical core
# needs nothing from this tree and 3.0 can delete both together. The v1
# surface (Capybara::Screenshot.window_size = ..., Diff.configure { ... },
# Diff.compare) keeps working unchanged: one storage, two views.
#
# Load order: requiring snap_diff/config first also eagerly evaluates the
# require-time defaults (ENV["CI"] for fail_if_new, Rails.root/pwd for
# root) at this same load moment, exactly when the old mattr_accessor
# default blocks used to run. snap_diff/config never requires back here,
# so the graph stays acyclic.
# default blocks used to run. Neither file requires back here, so the graph
# stays acyclic.
require "snap_diff/config"
# AVAILABLE_DRIVERS below is evaluated at class-body eval time, so Utils
# must be a real, already-loaded module before this module body runs.
require "snap_diff/utils"

module Capybara
module Screenshot
class << self
def active?
SnapDiff.config.active?
end

def screenshot_area
SnapDiff.config.screenshot_area
end

def screenshot_area_abs
SnapDiff.config.screenshot_area_abs
end
end

# Module to track screenshot changes
module Diff
AVAILABLE_DRIVERS = SnapDiff::Utils.detect_available_drivers.freeze

# Configure screenshot and diff settings in one block.
#
# Capybara::Screenshot::Diff.configure do |screenshot, diff|
# screenshot.window_size = [1280, 1024]
# screenshot.stability_time_limit = 1
# diff.driver = :vips
# diff.tolerance = 0.0005
# end
# The bare `yield` (rather than an explicit &block) keeps this
# method's published arity byte-identical to what it always had.
def self.configure
SnapDiff.start { |screenshot, diff| yield screenshot, diff }
end

def self.compare(baseline_path, current_path, **options)
SnapDiff.compare(baseline_path, current_path, **options)
end

def self.default_options
SnapDiff.config.default_options
end
end
end
end
require "snap_diff/legacy_shims"
18 changes: 6 additions & 12 deletions lib/capybara/screenshot/diff/image_compare.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@
require "snap_diff/comparison"
require "snap_diff/legacy_shims"

# Deliberately EAGER and silent (v2 step 6 exception): Comparison is a
# documented user-facing struct, so adopters feature-detect it with
# defined?/const_defined? -- neither of which triggers const_missing, so a
# lazy shim reported it permanently absent. See snap_diff/legacy_shims.rb
# for the full exception list.
module Capybara
module Screenshot
module Diff
Comparison = SnapDiff::Comparison::Images
end
end
end
# Capybara::Screenshot::Diff::Comparison (the images-holder struct) is a
# documented user-facing name that adopters feature-detect with
# defined?/const_defined?, so it is assigned EAGERLY rather than shimmed --
# const_defined? never triggers const_missing. That assignment now lives in
# snap_diff/legacy_shims (required above), with the rest of the v1 surface,
# so `require "snap_diff"` alone provides it too.
21 changes: 8 additions & 13 deletions lib/capybara/screenshot/diff/version.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
# frozen_string_literal: true

require "snap_diff/version"

# Deliberately EAGER and silent (v2 step 6 exception): the gemspec resolves
# Capybara::Screenshot::Diff::VERSION at build time, so a lazy warning shim
# would make every `gem build` warn. See snap_diff/legacy_shims.rb for the
# full exception list.
module Capybara
module Screenshot
module Diff
VERSION = SnapDiff::VERSION
end
end
end
# Capybara::Screenshot::Diff::VERSION is a documented name adopters read
# directly, so it is assigned EAGERLY rather than shimmed -- const_defined?
# never triggers const_missing. That assignment lives in
# snap_diff/legacy_shims (required below) with the rest of the v1 surface,
# because this file is no longer on any entry point's require path: the core
# reads SnapDiff::VERSION, and so does the gemspec. Assigning it here too
# would be a duplicate-constant warning, not a second safety net.
require "snap_diff/legacy_shims"
34 changes: 15 additions & 19 deletions lib/snap_diff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,25 @@ def self.assert_single_gem!(loaded_specs = Gem.loaded_specs)

# This lean entry must never load the umbrella "capybara_screenshot_diff"
# -- snap_diff_test.rb's "bare require never loads the umbrella" guard
# enforces it -- so nothing required below may reach back here.
# enforces it -- so nothing required below may reach back here. None of
# these requires reaches into lib/capybara* at all, so the canonical entry
# point is exactly what 3.0 keeps.
#
# The image_compare forwarder (not "snap_diff/comparison" directly) is
# deliberate: it installs snap_diff/legacy_shims, so the old
# Capybara::Screenshot::Diff constants stay resolvable, with deprecation
# warnings, in processes that only ever require "snap_diff".
# "capybara/dsl" is needed directly (not just transitively) so
# `Capybara.default_max_wait_time` in Config#default_options resolves even
# when "snap_diff" is required standalone (SnapDiffTest's
# "standalone-loadable in a fresh process" regression test).
#
# snap_diff/legacy_shims is deliberate and is the ONE line here that 3.0
# drops: it carries the whole v1 surface (const_missing forwarders, the old
# mattr_accessors, SnapDiff.start), so a process that only ever requires
# "snap_diff" still resolves the old Capybara::Screenshot::Diff names --
# with deprecation warnings -- exactly as it did when this file reached
# through the capybara/screenshot/diff/* forwarders to get them.
require "capybara/dsl"
require "capybara/screenshot/diff/config_legacy"
require "capybara/screenshot/diff/image_compare"
require "snap_diff/config"
require "snap_diff/comparison"
require "snap_diff/legacy_shims"
require "snap_diff/version"
# SnapDiff.session/.reset/.pending_screenshots_message are part of the
# documented core surface (docs/snapdiff.md object map lists them with no
Expand All @@ -63,18 +68,9 @@ def self.compare(baseline_path, current_path, **options)
Comparison.new(current_path, baseline_path, config.default_options.merge(options))
end

# v1-style configuration: yields the two legacy accessor holders
# (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+). Canonical home;
# +Capybara::Screenshot::Diff.configure+ forwards here, and both names
# stay identical in call shape.
#
# SnapDiff.start do |screenshot, diff|
# screenshot.window_size = [1280, 1024]
# diff.tolerance = 0.0005
# end
def self.start
yield Capybara::Screenshot, Capybara::Screenshot::Diff
end
# SnapDiff.start -- the v1-shaped two-holder config block -- is defined in
# snap_diff/legacy_shims (required above), because the holders it yields
# are the v1 surface and it cannot outlive them.

# Forward-looking configuration: yields the single consolidated
# {SnapDiff::Config} object instead of the two old holders. Same
Expand Down
8 changes: 5 additions & 3 deletions lib/snap_diff/browser_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
module SnapDiff
module BrowserHelpers
def self.resize_window_if_needed
if ::Capybara::Screenshot.respond_to?(:window_size) && ::Capybara::Screenshot.window_size
resize_to(::Capybara::Screenshot.window_size)
end
# The respond_to? guard this replaced existed because the legacy
# mattr_accessor might not be installed yet; Config always has the
# attribute, so only the value matters now.
window_size = SnapDiff.config.window_size
resize_to(window_size) if window_size
end

def self.resize_to(window_size)
Expand Down
Loading
Loading