From 3dd9078ebec2cf98c5d6469b185aed53d57e2152 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:41:02 +0200 Subject: [PATCH 1/5] test: move the v1-surface tests into test/legacy/ and add rake test:canonical The five tests whose SUBJECT is the v1 compatibility surface now live in test/legacy/, so the 3.0 deletion is one more path on the same git rm: git rm -r lib/capybara* ... test/legacy A directory rather than a list in the Rakefile: nothing to keep in sync. - rake test unchanged, runs everything (today's gate) - rake test:canonical NEW, everything except test/legacy (the 3.0 gate) - rake test:unit test/unit + test/legacy, so the release gate keeps its coverage (legacy/ marks lifetime, not kind) errors_alias_test.rb was mixed: the four CapybaraScreenshotDiff::* alias pairs are v1 surface, the hierarchy assertions outlive them. Split rather than moved whole -- test/unit/errors_test.rb keeps the two canonical tests verbatim, so no assertion is lost at 3.0. 530 runs, 1519 assertions, 0 failures (unchanged). --- Rakefile | 32 ++++++++++++++++- test/{unit => legacy}/errors_alias_test.rb | 32 ++++------------- .../legacy_namespace_deprecation_test.rb | 5 ++- .../legacy_tree_is_alias_only_test.rb | 4 +++ .../namespace_forwarding_test.rb | 6 ++++ .../snap_diff_deprecation_test.rb | 3 ++ test/unit/errors_test.rb | 34 +++++++++++++++++++ 7 files changed, 89 insertions(+), 27 deletions(-) rename test/{unit => legacy}/errors_alias_test.rb (62%) rename test/{unit => legacy}/legacy_namespace_deprecation_test.rb (96%) rename test/{unit => legacy}/legacy_tree_is_alias_only_test.rb (97%) rename test/{unit => legacy}/namespace_forwarding_test.rb (95%) rename test/{unit => legacy}/snap_diff_deprecation_test.rb (95%) create mode 100644 test/unit/errors_test.rb diff --git a/Rakefile b/Rakefile index 0a752005..c0c705ed 100644 --- a/Rakefile +++ b/Rakefile @@ -5,16 +5,46 @@ require "rake/testtask" task default: :test +# THE 3.0 SPLIT. +# +# test/legacy/ holds every test whose SUBJECT is the v1 compatibility surface +# -- the old Capybara::Screenshot / CapybaraScreenshotDiff namespaces, their +# deprecation warnings, and the gates that keep lib/capybara* alias-only. +# Those tests guard the v1 contract for the whole 2.x line, so they stay and +# stay green; in 3.0 they are deleted by the same commit that deletes what +# they test: +# +# git rm -r lib/capybara* lib/capybara_screenshot_diff.rb \ +# lib/snap_diff/legacy_shims.rb lib/snap_diff/deprecation.rb \ +# test/legacy +# +# A directory rather than a list in this file: there is nothing to keep in +# sync, and the deletion is one `git rm -r`. +# +# `rake test` -- everything, today's gate. +# `rake test:canonical` -- exactly what must still pass once test/legacy and +# the v1 trees are gone. THE 3.0 GATE. +# `rake test:unit` -- unit-sized tests; test/legacy is unit-sized too +# (legacy/ marks lifetime, not kind), so it is in. +LEGACY_SURFACE_TESTS = "test/legacy/**/*_test.rb" + Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end +desc "Run every test that must survive the 3.0 deletion of the v1 surface" +Rake::TestTask.new("test:canonical") do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"].exclude(LEGACY_SURFACE_TESTS) +end + Rake::TestTask.new("test:unit") do |t| t.libs << "test" t.libs << "lib" - t.test_files = FileList["test/unit/**/*_test.rb"] + t.test_files = FileList["test/unit/**/*_test.rb", LEGACY_SURFACE_TESTS] end Rake::TestTask.new("test:integration") do |t| diff --git a/test/unit/errors_alias_test.rb b/test/legacy/errors_alias_test.rb similarity index 62% rename from test/unit/errors_alias_test.rb rename to test/legacy/errors_alias_test.rb index 8613b1b3..40eaed8c 100644 --- a/test/unit/errors_alias_test.rb +++ b/test/legacy/errors_alias_test.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "test_helper" +# The shared harness loads canonical entry points only, so a legacy-surface +# test pulls in the v1 entry itself -- the require goes with the file in 3.0. +require "capybara_screenshot_diff" # ADR-008 step 2: the error classes live in SnapDiff (snap_diff/errors); # the old CapybaraScreenshotDiff names are EAGER same-object aliases -- @@ -9,6 +12,10 @@ # Note the absence of any deprecation-silencing here: eager aliases never # warn, and the suite-wide guard in test_helper raises on unexpected # warnings, so these tests double as proof the aliases stay warning-free. +# +# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* +# in 3.0. The hierarchy assertions that outlive the aliases moved to +# test/unit/errors_test.rb. class ErrorsAliasTest < ActiveSupport::TestCase # old constant path => new constant path MAPPING = { @@ -44,29 +51,4 @@ class ErrorsAliasTest < ActiveSupport::TestCase end assert_equal "probe", caught.message end - - test "error hierarchy is preserved" do - assert_operator SnapDiff::ExpectationNotMet, :<, SnapDiff::Error - assert_operator SnapDiff::UnstableImage, :<, SnapDiff::Error - assert_operator SnapDiff::Error, :<, SnapDiff::ErrorWithFilteredBacktrace - assert_operator SnapDiff::WindowSizeMismatchError, :<, SnapDiff::ErrorWithFilteredBacktrace - end - - # docs/snapdiff.md calls SnapDiff::Error "Base class for every error this - # gem raises" -- so `rescue SnapDiff::Error` has to actually catch every - # one of them. Discovered rather than listed: a new error class added - # outside the hierarchy fails here instead of quietly breaking that claim - # for adopters (WindowSizeMismatchError and DualInstallError both did). - test "every error the gem defines inherits SnapDiff::Error" do - plumbing = [SnapDiff::Error, SnapDiff::ErrorWithFilteredBacktrace] - errors = SnapDiff.constants - .map { |name| SnapDiff.const_get(name) } - .select { |const| const.is_a?(Class) && const < StandardError } - plumbing - - assert_operator errors.size, :>=, 4, "probe should see the gem's error classes" - - errors.each do |error| - assert_operator error, :<, SnapDiff::Error, "#{error} must inherit SnapDiff::Error" - end - end end diff --git a/test/unit/legacy_namespace_deprecation_test.rb b/test/legacy/legacy_namespace_deprecation_test.rb similarity index 96% rename from test/unit/legacy_namespace_deprecation_test.rb rename to test/legacy/legacy_namespace_deprecation_test.rb index c1c03a86..b8753695 100644 --- a/test/unit/legacy_namespace_deprecation_test.rb +++ b/test/legacy/legacy_namespace_deprecation_test.rb @@ -3,7 +3,7 @@ require "test_helper" require "open3" require "snap_diff/deprecation" -require "unit/namespace_forwarding_test" # single source of truth for the old->new MAPPING +require "legacy/namespace_forwarding_test" # single source of truth for the old->new MAPPING # ADR-004 v2 step 6: resolving an old-namespace constant emits a deprecation # warning -- exactly once per constant per process, naming the SnapDiff @@ -11,6 +11,9 @@ # switches (SnapDiff.silence_deprecations / SNAP_DIFF_SILENCE_DEPRECATIONS). # Same-object identity for every pair stays pinned by # namespace_forwarding_test.rb; this file pins only the warning behavior. +# +# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* +# and snap_diff/deprecation.rb in 3.0. class LegacyNamespaceDeprecationTest < ActiveSupport::TestCase # Documented exceptions that stay EAGER (real constants, never warn): # - Os / DSL: advertised entry-point constants, probed with diff --git a/test/unit/legacy_tree_is_alias_only_test.rb b/test/legacy/legacy_tree_is_alias_only_test.rb similarity index 97% rename from test/unit/legacy_tree_is_alias_only_test.rb rename to test/legacy/legacy_tree_is_alias_only_test.rb index 29d3f741..28e87030 100644 --- a/test/unit/legacy_tree_is_alias_only_test.rb +++ b/test/legacy/legacy_tree_is_alias_only_test.rb @@ -10,6 +10,10 @@ # and one-line forwarders. If that stays true, dropping v1 support in 3.0 is # a deletion; the moment real logic lands back in these trees it becomes a # refactor. This test fails the second that happens, naming the file. +# +# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with the trees it +# scans in 3.0. Its mirror image, core_tree_has_no_legacy_deps_test.rb, +# guards what 3.0 KEEPS and so stays in test/unit/. class LegacyTreeIsAliasOnlyTest < ActiveSupport::TestCase LIB = Pathname.new(__dir__).join("../../lib").expand_path diff --git a/test/unit/namespace_forwarding_test.rb b/test/legacy/namespace_forwarding_test.rb similarity index 95% rename from test/unit/namespace_forwarding_test.rb rename to test/legacy/namespace_forwarding_test.rb index 99989c06..0fbe813a 100644 --- a/test/unit/namespace_forwarding_test.rb +++ b/test/legacy/namespace_forwarding_test.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "test_helper" +# The shared harness loads canonical entry points only, so a legacy-surface +# test pulls in the v1 entry itself -- the require goes with the file in 3.0. +require "capybara_screenshot_diff" # Every old-namespace constant touched by the ADR-004 v2 file-tree move # must forward to the exact same object as its SnapDiff:: replacement -- @@ -8,6 +11,9 @@ # breaks (wrong target, deleted alias, typo), this fails loudly instead # of surfacing as a mysterious downstream `NameError` or a comparison # that always returns false. +# +# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* +# in 3.0, when there is no old namespace left to forward. class NamespaceForwardingTest < ActiveSupport::TestCase # This file's whole purpose is resolving the old names, so silence the # shims' deprecation warnings here (the suite-wide guard in test_helper diff --git a/test/unit/snap_diff_deprecation_test.rb b/test/legacy/snap_diff_deprecation_test.rb similarity index 95% rename from test/unit/snap_diff_deprecation_test.rb rename to test/legacy/snap_diff_deprecation_test.rb index 619f3465..0696c8b8 100644 --- a/test/unit/snap_diff_deprecation_test.rb +++ b/test/legacy/snap_diff_deprecation_test.rb @@ -3,6 +3,9 @@ require "test_helper" require "snap_diff/deprecation" +# LEGACY SURFACE (test/legacy/, see the Rakefile): SnapDiff::Deprecation is +# the channel that announces the v1 shims, so snap_diff/deprecation.rb and +# this file are deleted together with lib/capybara* in 3.0. class SnapDiffDeprecationTest < ActiveSupport::TestCase def setup SnapDiff::Deprecation.reset! diff --git a/test/unit/errors_test.rb b/test/unit/errors_test.rb new file mode 100644 index 00000000..b38b4ed4 --- /dev/null +++ b/test/unit/errors_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "test_helper" + +# The canonical half of what used to be errors_alias_test.rb: the shape of +# the SnapDiff error hierarchy itself, which outlives the v1 aliases. The +# `CapybaraScreenshotDiff::*` alias half stayed behind in +# test/legacy/errors_alias_test.rb and goes with the v1 trees in 3.0. +class ErrorsTest < ActiveSupport::TestCase + test "error hierarchy is preserved" do + assert_operator SnapDiff::ExpectationNotMet, :<, SnapDiff::Error + assert_operator SnapDiff::UnstableImage, :<, SnapDiff::Error + assert_operator SnapDiff::Error, :<, SnapDiff::ErrorWithFilteredBacktrace + assert_operator SnapDiff::WindowSizeMismatchError, :<, SnapDiff::ErrorWithFilteredBacktrace + end + + # docs/snapdiff.md calls SnapDiff::Error "Base class for every error this + # gem raises" -- so `rescue SnapDiff::Error` has to actually catch every + # one of them. Discovered rather than listed: a new error class added + # outside the hierarchy fails here instead of quietly breaking that claim + # for adopters (WindowSizeMismatchError and DualInstallError both did). + test "every error the gem defines inherits SnapDiff::Error" do + plumbing = [SnapDiff::Error, SnapDiff::ErrorWithFilteredBacktrace] + errors = SnapDiff.constants + .map { |name| SnapDiff.const_get(name) } + .select { |const| const.is_a?(Class) && const < StandardError } - plumbing + + assert_operator errors.size, :>=, 4, "probe should see the gem's error classes" + + errors.each do |error| + assert_operator error, :<, SnapDiff::Error, "#{error} must inherit SnapDiff::Error" + end + end +end From b067778f138831651441e554d23b234238d72200 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:53:15 +0200 Subject: [PATCH 2/5] test: point the whole canonical suite at SnapDiff names The suite still spoke v1 everywhere, so it would have broken on the 3.0 deletion even though the gem no longer does. Mechanical, no behaviour and no assertion values changed: - harness: test_helper + system_test_case load snap_diff/integrations/* and configure through SnapDiff.config; the support stubs (DSLStub, ScreenshoterStub, TestDoubles, DriverCoverage, NonMinitest) stop reopening gem namespaces and become plain top-level modules - 33 test files were defined inside module Capybara::Screenshot(::Diff) / CapybaraScreenshotDiff -- de-nested to top-level classes, so no bare constant resolves into a namespace 3.0 deletes - 270 legacy constant/accessor/session call sites repointed (CapybaraScreenshotDiff.registry -> SnapDiff.session, .reporters -> SnapDiff::Reporting.reporters, Capybara::Screenshot.root -> SnapDiff.config.root, ...) and 25 legacy require paths - legacy-surface tests now require the v1 entry point themselves, since the shared harness no longer loads it Three claims would have become tautologies under a blind repoint (assert_same SnapDiff.session, SnapDiff.session and friends): they were forwarder-identity claims about the v1 view. Preserved verbatim in the new test/legacy/legacy_forwarders_test.rb together with SnapDiff.start, which yields the two v1 holders and cannot outlive them. rake test:unit 534 runs, 1526 assertions, 0F/0E (was 530/1519) rake test 562 runs, 1571 assertions, 0F/0E/1S (was 558/1564) +4 runs: legacy_forwarders_test keeps the v1 claim where the canonical file also kept its own version (register-appends, reporters_mutex, serve). --- .../rspec_after_hook_order_masking_spec.rb | 18 +- test/fixtures/rspec_pending_masking_spec.rb | 18 +- test/fixtures/rspec_spec.rb | 18 +- test/integration/browser_screenshot_test.rb | 388 +++++++++-------- test/integration/record_screenshot_test.rb | 12 +- .../rspec_after_hook_order_masking_test.rb | 74 ++-- .../integration/rspec_pending_masking_test.rb | 68 ++- test/integration/rspec_test.rb | 20 +- test/integration/test_methods_system_test.rb | 30 +- test/legacy/legacy_forwarders_test.rb | 98 +++++ .../capybara_screenshot_diff/dsl_stub.rb | 65 --- test/support/driver_coverage.rb | 51 +-- test/support/dsl_stub.rb | 65 +++ test/support/non_minitest_assertions.rb | 28 +- test/support/screenshoter_stub.rb | 4 +- test/support/setup_capybara_drivers.rb | 4 +- test/support/stub_test_methods.rb | 2 +- test/support/test_doubles.rb | 242 ++++++----- test/support/test_helpers.rb | 4 +- test/system_test_case.rb | 56 +-- test/test_helper.rb | 20 +- test/unit/annotation_service_test.rb | 102 +++-- test/unit/area_calculator_test.rb | 98 +++-- test/unit/attempts_reporter_test.rb | 6 +- test/unit/backtrace_filter_test.rb | 84 ++-- test/unit/capture/viewport_test.rb | 4 +- test/unit/compare_api_test.rb | 105 +++-- test/unit/config_default_timing_test.rb | 2 +- .../unit/core_tree_has_no_legacy_deps_test.rb | 2 +- test/unit/diff_test.rb | 390 +++++++++--------- test/unit/difference_test.rb | 38 +- test/unit/driver_coverage_test.rb | 68 ++- test/unit/drivers/chunky_png_driver_test.rb | 8 +- test/unit/drivers/utils_test.rb | 82 ++-- test/unit/drivers/vips_driver_test.rb | 6 +- test/unit/drivers_test.rb | 2 +- test/unit/dsl_test.rb | 380 +++++++++-------- test/unit/image_compare_test.rb | 372 ++++++++--------- test/unit/image_preprocessor_test.rb | 128 +++--- test/unit/minitest_assertions_test.rb | 102 +++-- test/unit/pending_screenshots_message_test.rb | 70 ++-- test/unit/region_test.rb | 112 +++-- test/unit/registry_concurrency_test.rb | 177 ++++---- test/unit/reporter_interplay_test.rb | 178 ++++---- test/unit/reporters/default_test.rb | 92 ++--- test/unit/reporters/html_reporter_test.rb | 380 +++++++++-------- test/unit/reporters_mutex_test.rb | 104 +++-- test/unit/screenshot_assertion_test.rb | 12 +- test/unit/screenshot_matcher_test.rb | 370 ++++++++--------- test/unit/screenshot_namer_test.rb | 184 ++++----- test/unit/screenshot_test.rb | 34 +- test/unit/screenshoter_test.rb | 80 ++-- test/unit/snap_diff_config_test.rb | 56 +-- test/unit/snap_diff_test.rb | 39 +- test/unit/snap_manager_cleanup_test.rb | 122 +++--- test/unit/snap_manager_test.rb | 174 ++++---- test/unit/stable_screenshoter_test.rb | 224 +++++----- test/unit/static_test.rb | 45 +- test/unit/support_load_probe_test.rb | 26 +- test/unit/vcs_test.rb | 58 ++- 60 files changed, 2880 insertions(+), 2921 deletions(-) create mode 100644 test/legacy/legacy_forwarders_test.rb delete mode 100644 test/support/capybara_screenshot_diff/dsl_stub.rb create mode 100644 test/support/dsl_stub.rb diff --git a/test/fixtures/rspec_after_hook_order_masking_spec.rb b/test/fixtures/rspec_after_hook_order_masking_spec.rb index 8b876072..1713a905 100644 --- a/test/fixtures/rspec_after_hook_order_masking_spec.rb +++ b/test/fixtures/rspec_after_hook_order_masking_spec.rb @@ -16,7 +16,7 @@ end end -require "capybara_screenshot_diff/rspec" +require "snap_diff/integrations/rspec" require "support/stub_test_methods" unless defined?(SCREEN_SIZE) @@ -40,18 +40,18 @@ before do Capybara.current_driver = Capybara.javascript_driver Capybara.page.current_window.resize_to(*SCREEN_SIZE) - Capybara::Screenshot.window_size = SCREEN_SIZE + SnapDiff.config.window_size = SCREEN_SIZE - Capybara::Screenshot.save_path = "doc/screenshots" - Capybara::Screenshot.root = Rails.root / "../test/fixtures/app" - Capybara::Screenshot.add_os_path = true - Capybara::Screenshot.add_driver_path = true - Capybara::Screenshot::Diff.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym - Capybara::Screenshot::Diff.tolerance = 0.5 + SnapDiff.config.save_path = "doc/screenshots" + SnapDiff.config.root = Rails.root / "../test/fixtures/app" + SnapDiff.config.add_os_path = true + SnapDiff.config.add_driver_path = true + SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym + SnapDiff.config.tolerance = 0.5 # This fixture runs standalone in its own subprocess (no # ActiveSupport::TestCase setup forcing this off), and CI sets $CI, # which flips the default on and would raise before we ever get here. - Capybara::Screenshot::Diff.fail_if_new = false + SnapDiff.config.fail_if_new = false end it "keeps a real after-hook failure failing even when a new screenshot is pending" do diff --git a/test/fixtures/rspec_pending_masking_spec.rb b/test/fixtures/rspec_pending_masking_spec.rb index 5a321c9c..766e8de0 100644 --- a/test/fixtures/rspec_pending_masking_spec.rb +++ b/test/fixtures/rspec_pending_masking_spec.rb @@ -2,7 +2,7 @@ require "capybara/rspec" -require "capybara_screenshot_diff/rspec" +require "snap_diff/integrations/rspec" require "support/stub_test_methods" unless defined?(SCREEN_SIZE) @@ -27,18 +27,18 @@ before do Capybara.current_driver = Capybara.javascript_driver Capybara.page.current_window.resize_to(*SCREEN_SIZE) - Capybara::Screenshot.window_size = SCREEN_SIZE + SnapDiff.config.window_size = SCREEN_SIZE - Capybara::Screenshot.save_path = "doc/screenshots" - Capybara::Screenshot.root = Rails.root / "../test/fixtures/app" - Capybara::Screenshot.add_os_path = true - Capybara::Screenshot.add_driver_path = true - Capybara::Screenshot::Diff.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym - Capybara::Screenshot::Diff.tolerance = 0.5 + SnapDiff.config.save_path = "doc/screenshots" + SnapDiff.config.root = Rails.root / "../test/fixtures/app" + SnapDiff.config.add_os_path = true + SnapDiff.config.add_driver_path = true + SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym + SnapDiff.config.tolerance = 0.5 # This fixture runs standalone in its own subprocess (no # ActiveSupport::TestCase setup forcing this off), and CI sets $CI, # which flips the default on and would raise before we ever get here. - Capybara::Screenshot::Diff.fail_if_new = false + SnapDiff.config.fail_if_new = false end it "keeps a genuine failure failing even when a new screenshot is pending" do diff --git a/test/fixtures/rspec_spec.rb b/test/fixtures/rspec_spec.rb index d3781cfd..395f4c52 100644 --- a/test/fixtures/rspec_spec.rb +++ b/test/fixtures/rspec_spec.rb @@ -2,7 +2,7 @@ require "capybara/rspec" -require "capybara_screenshot_diff/rspec" +require "snap_diff/integrations/rspec" require "support/stub_test_methods" unless defined?(SCREEN_SIZE) @@ -14,14 +14,14 @@ before do Capybara.current_driver = Capybara.javascript_driver Capybara.page.current_window.resize_to(*SCREEN_SIZE) - Capybara::Screenshot.window_size = SCREEN_SIZE - - Capybara::Screenshot.save_path = "doc/screenshots" - Capybara::Screenshot.root = Rails.root / "../test/fixtures/app" - Capybara::Screenshot.add_os_path = true - Capybara::Screenshot.add_driver_path = true - Capybara::Screenshot::Diff.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym - Capybara::Screenshot::Diff.tolerance = 0.5 + SnapDiff.config.window_size = SCREEN_SIZE + + SnapDiff.config.save_path = "doc/screenshots" + SnapDiff.config.root = Rails.root / "../test/fixtures/app" + SnapDiff.config.add_os_path = true + SnapDiff.config.add_driver_path = true + SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym + SnapDiff.config.tolerance = 0.5 end it "should include CapybaraScreenshotDiff in rspec" do diff --git a/test/integration/browser_screenshot_test.rb b/test/integration/browser_screenshot_test.rb index c7cb2837..5d83bca1 100644 --- a/test/integration/browser_screenshot_test.rb +++ b/test/integration/browser_screenshot_test.rb @@ -2,280 +2,278 @@ require "system_test_case" -module Capybara::Screenshot - class BrowserScreenshotTest < SystemTestCase - setup do - Capybara::Screenshot.blur_active_element = true - @original_tolerance = Capybara::Screenshot::Diff.tolerance - Capybara::Screenshot::Diff.tolerance = (Capybara::Screenshot::Diff.driver == :vips) ? 0.035 : 0.13 - end +class BrowserScreenshotTest < SystemTestCase + setup do + SnapDiff.config.blur_active_element = true + @original_tolerance = SnapDiff.config.tolerance + SnapDiff.config.tolerance = (SnapDiff.config.driver == :vips) ? 0.035 : 0.13 + end - teardown do - Capybara::Screenshot.blur_active_element = nil - Capybara::Screenshot::Diff.tolerance = @original_tolerance - end + teardown do + SnapDiff.config.blur_active_element = nil + SnapDiff.config.tolerance = @original_tolerance + end - def before_teardown - if CapybaraScreenshotDiff.assertions_present? - # NOTE: We rollback new screenshots in order to remain their original state - # and only for debug mode we keep them - unless persist_comparisons? - CapybaraScreenshotDiff.assertions.each(&method(:rollback_comparison_runtime_files)) - end - # NOTE: We clear tracked different errors in order to not raise error - CapybaraScreenshotDiff.reset + def before_teardown + if SnapDiff.session.assertions_present? + # NOTE: We rollback new screenshots in order to remain their original state + # and only for debug mode we keep them + unless persist_comparisons? + SnapDiff.session.assertions.each(&method(:rollback_comparison_runtime_files)) end - super + # NOTE: We clear tracked different errors in order to not raise error + SnapDiff.reset end + super + end + + def test_screenshot_without_changes + visit "/" + assert_matches_screenshot "index" + end - def test_screenshot_without_changes - visit "/" - assert_matches_screenshot "index" + def test_screenshot_with_changes + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end + visit "/" - def test_screenshot_with_changes - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end - visit "/" + fill_in "First Field:", with: "Some changes in the field" - fill_in "First Field:", with: "Some changes in the field" + assert_matches_screenshot("index", tolerance: nil) + assert_screenshot_error_for("index") + end - assert_matches_screenshot("index", tolerance: nil) - assert_screenshot_error_for("index") - end + def test_window_size_should_resize_browser_window_in_setup + assert_equal SCREEN_SIZE, window_size + end - def test_window_size_should_resize_browser_window_in_setup - assert_equal SCREEN_SIZE, window_size - end + def test_screenshot_with_hide_caret_enabled + SnapDiff.config.hide_caret = true + visit "/" - def test_screenshot_with_hide_caret_enabled - Capybara::Screenshot.hide_caret = true - visit "/" + fill_in "First Field:", with: "Test Input With Hide Caret" - fill_in "First Field:", with: "Test Input With Hide Caret" + assert_matches_screenshot("index-hide_caret-enabled") + ensure + SnapDiff.config.hide_caret = nil + end - assert_matches_screenshot("index-hide_caret-enabled") - ensure - Capybara::Screenshot.hide_caret = nil - end + def test_screenshot_with_hide_caret_disabled + SnapDiff.config.hide_caret = false - def test_screenshot_with_hide_caret_disabled - Capybara::Screenshot.hide_caret = false + visit "/" + fill_in "First Field:", with: "Test Input Without Hide Caret" - visit "/" - fill_in "First Field:", with: "Test Input Without Hide Caret" + # Hide caret is flaky issue, let's give more tries to take stable screenshot + assert_matches_screenshot "index-hide_caret-disabled", wait: Capybara.default_max_wait_time * 5 + ensure + SnapDiff.config.hide_caret = nil + end - # Hide caret is flaky issue, let's give more tries to take stable screenshot - assert_matches_screenshot "index-hide_caret-disabled", wait: Capybara.default_max_wait_time * 5 - ensure - Capybara::Screenshot.hide_caret = nil - end + def test_screenshot_with_blur_active_element_enabled + SnapDiff.config.blur_active_element = true + visit "/" + fill_in "First Field:", with: "Test Input With Hide Caret" - def test_screenshot_with_blur_active_element_enabled - Capybara::Screenshot.blur_active_element = true - visit "/" - fill_in "First Field:", with: "Test Input With Hide Caret" + assert_matches_screenshot "index-blur_active_element-enabled" + ensure + SnapDiff.config.blur_active_element = nil + end - assert_matches_screenshot "index-blur_active_element-enabled" - ensure - Capybara::Screenshot.blur_active_element = nil - end + def test_screenshot_with_blur_active_element_disabled + SnapDiff.config.blur_active_element = false + visit "/" + fill_in "First Field:", with: "Test Input Without Hide Caret" - def test_screenshot_with_blur_active_element_disabled - Capybara::Screenshot.blur_active_element = false - visit "/" - fill_in "First Field:", with: "Test Input Without Hide Caret" + assert_matches_screenshot "index-blur_active_element-disabled" + ensure + SnapDiff.config.blur_active_element = nil + end - assert_matches_screenshot "index-blur_active_element-disabled" - ensure - Capybara::Screenshot.blur_active_element = nil - end + def test_screenshot_selected_element + visit "/" - def test_screenshot_selected_element - visit "/" + assert_matches_screenshot "cropped_screenshot", crop: [0, 100, 100, 200] + end - assert_matches_screenshot "cropped_screenshot", crop: [0, 100, 100, 200] + test "skip_area accepts passing multiple coordinates as one array" do + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end - test "skip_area accepts passing multiple coordinates as one array" do - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end + visit "/" + fill_in "First Field:", with: "Changed" + fill_in "Second Field:", with: "Changed" - visit "/" - fill_in "First Field:", with: "Changed" - fill_in "Second Field:", with: "Changed" + assert_matches_screenshot("index", skip_area: [8, 100, 218, 140, 8, 140, 218, 180]) - assert_matches_screenshot("index", skip_area: [8, 100, 218, 140, 8, 140, 218, 180]) + assert_no_screenshot_errors + end - assert_no_screenshot_errors - end + test "compare crops only when other part is not working" do + visit "/index-without-img.html" - test "compare crops only when other part is not working" do - visit "/index-without-img.html" + assert_matches_screenshot("index-without-img-cropped", crop: "form", color_distance_limit: 40) - assert_matches_screenshot("index-without-img-cropped", crop: "form", color_distance_limit: 40) + assert_no_screenshot_errors + end - assert_no_screenshot_errors - end + test "crop accepts css selector" do + visit "/index-without-img.html" - test "crop accepts css selector" do - visit "/index-without-img.html" + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" + end - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end + assert_matches_screenshot("index-without-img-cropped", crop: "form") - assert_matches_screenshot("index-without-img-cropped", crop: "form") + assert_no_screenshot_errors + end - assert_no_screenshot_errors - end + test "skip_area accepts css selector" do + visit "/" - test "skip_area accepts css selector" do - visit "/" + assert_matches_screenshot("index_with_skip_area_as_array_of_css", skip_area: ["form"]) + assert_matches_screenshot("index_with_skip_area_as_array_of_css_and_p", skip_area: [[90, 950, 180, 1000], "form"]) + end - assert_matches_screenshot("index_with_skip_area_as_array_of_css", skip_area: ["form"]) - assert_matches_screenshot("index_with_skip_area_as_array_of_css_and_p", skip_area: [[90, 950, 180, 1000], "form"]) + test "skip_area accepts css selector and ignores changes" do + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end - test "skip_area accepts css selector and ignores changes" do - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end + visit "/" - visit "/" + fill_in "First Field:", with: "Changed" + fill_in "Second Field:", with: "Changed" - fill_in "First Field:", with: "Changed" - fill_in "Second Field:", with: "Changed" + assert_matches_screenshot("index", skip_area: "form") - assert_matches_screenshot("index", skip_area: "form") + assert_no_screenshot_errors + end - assert_no_screenshot_errors - end + test "cropped screenshot" do + visit "/index.html" - test "cropped screenshot" do - visit "/index.html" + assert_matches_screenshot("index-cropped", skip_area: "#first-field", crop: "form") + end - assert_matches_screenshot("index-cropped", skip_area: "#first-field", crop: "form") + test "skip_area converts coordinates to be relative to cropped region" do + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end - test "skip_area converts coordinates to be relative to cropped region" do - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end + visit "/index.html" + fill_in "First Field:", with: "New Change" + fill_in "Second Field:", with: "New Change" - visit "/index.html" - fill_in "First Field:", with: "New Change" - fill_in "Second Field:", with: "New Change" + assert_matches_screenshot("index-cropped", skip_area: "#first-field", crop: "form", tolerance: 0.001) - assert_matches_screenshot("index-cropped", skip_area: "#first-field", crop: "form", tolerance: 0.001) + assert_not_predicate( + SnapDiff.session.failed_assertions, + :empty?, + "differences have not been found when they should have been" + ) + end - assert_not_predicate( - CapybaraScreenshotDiff.failed_assertions, - :empty?, - "differences have not been found when they should have been" - ) + test "skip_area by css selectors" do + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end - test "skip_area by css selectors" do - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end + visit "/" + fill_in "First Field:", with: "Test Input With Hide Caret" - visit "/" - fill_in "First Field:", with: "Test Input With Hide Caret" + assert_matches_screenshot("index", skip_area: "form") + assert_no_screenshot_errors + end - assert_matches_screenshot("index", skip_area: "form") - assert_no_screenshot_errors + test "crop and skip_area by css selectors" do + if ENV["RECORD_SCREENSHOTS"] + skip "we record screenshots only in" end - test "crop and skip_area by css selectors" do - if ENV["RECORD_SCREENSHOTS"] - skip "we record screenshots only in" - end - - visit "/index-without-img.html" - fill_in "First Field:", with: "Test Input With Hide Caret" + visit "/index-without-img.html" + fill_in "First Field:", with: "Test Input With Hide Caret" - assert_matches_screenshot("index-without-img-cropped", skip_area: "input", crop: "form") + assert_matches_screenshot("index-without-img-cropped", skip_area: "input", crop: "form") - assert_no_screenshot_errors - end + assert_no_screenshot_errors + end - test "bounds_for_css for multiple elements returns all areas" do - visit "/" + test "bounds_for_css for multiple elements returns all areas" do + visit "/" - label_bounds = SnapDiff::BrowserHelpers.bounds_for_css("label") + label_bounds = SnapDiff::BrowserHelpers.bounds_for_css("label") - assert_equal 2, label_bounds.size - end + assert_equal 2, label_bounds.size + end - test "rect_for for multiple elements returns first visible element" do - visit "/index.html" + test "rect_for for multiple elements returns first visible element" do + visit "/index.html" - label_bound = rect_for("label") + label_bound = rect_for("label") - assert_equal 4, label_bound.size - end + assert_equal 4, label_bound.size + end - test "animated example" do - optional_test + test "animated example" do + optional_test - visit "/index-with-anim.html" + visit "/index-with-anim.html" - assert_raises CapybaraScreenshotDiff::UnstableImage, "Could not get stable screenshot within 0.5s:" do - # We need to run several times, - # because quick_equal could produce incorrect result, - # because of the same size screenshots - 10.times do - assert_matches_screenshot "index-with-anim", stability_time_limit: 0.33, wait: 0.5, tolerance: nil - end + assert_raises SnapDiff::UnstableImage, "Could not get stable screenshot within 0.5s:" do + # We need to run several times, + # because quick_equal could produce incorrect result, + # because of the same size screenshots + 10.times do + assert_matches_screenshot "index-with-anim", stability_time_limit: 0.33, wait: 0.5, tolerance: nil end - ensure - SnapDiff::SnapManager.snapshot("index-with-anim").delete! end + ensure + SnapDiff::SnapManager.snapshot("index-with-anim").delete! + end - def test_await_all_images_are_loaded - visit "/index.html" - assert_raises ::Minitest::Assertion do - SnapDiff::BrowserHelpers.stub(:pending_image_to_load, "http://127.0.0.1:62815/image.png") do - assert_matches_screenshot :index - end + def test_await_all_images_are_loaded + visit "/index.html" + assert_raises ::Minitest::Assertion do + SnapDiff::BrowserHelpers.stub(:pending_image_to_load, "http://127.0.0.1:62815/image.png") do + assert_matches_screenshot :index end - assert_no_screenshot_errors end + assert_no_screenshot_errors + end - private - - def rect_for(css_selector) - SnapDiff::BrowserHelpers.all_visible_regions_for(css_selector).first - end + private - def window_size - if page.driver.respond_to?(:window_size) - return page.driver.window_size(page.driver.current_window_handle) - end + def rect_for(css_selector) + SnapDiff::BrowserHelpers.all_visible_regions_for(css_selector).first + end - page.driver.browser.manage.window.size.to_a + def window_size + if page.driver.respond_to?(:window_size) + return page.driver.window_size(page.driver.current_window_handle) end - def assert_screenshot_error_for(screenshot_name) - assertions = CapybaraScreenshotDiff.failed_assertions + page.driver.browser.manage.window.size.to_a + end - assert_equal 1, assertions&.length, "expecting to have just one difference" - assert_equal screenshot_name, assertions[0].name, "index screenshot should have difference for changed page" - end + def assert_screenshot_error_for(screenshot_name) + assertions = SnapDiff.session.failed_assertions - def assert_no_screenshot_errors - screenshots = CapybaraScreenshotDiff.failed_assertions + assert_equal 1, assertions&.length, "expecting to have just one difference" + assert_equal screenshot_name, assertions[0].name, "index screenshot should have difference for changed page" + end - error_messages = screenshots.map { |assertion| assertion.compare.error_message } + def assert_no_screenshot_errors + screenshots = SnapDiff.session.failed_assertions - assert( - screenshots.empty?, - "expecting not to have any difference. But got next:\n\n#{error_messages.join(";\n")}" - ) - end + error_messages = screenshots.map { |assertion| assertion.compare.error_message } + + assert( + screenshots.empty?, + "expecting not to have any difference. But got next:\n\n#{error_messages.join(";\n")}" + ) end end diff --git a/test/integration/record_screenshot_test.rb b/test/integration/record_screenshot_test.rb index 793ff51d..2bb2d747 100644 --- a/test/integration/record_screenshot_test.rb +++ b/test/integration/record_screenshot_test.rb @@ -4,16 +4,16 @@ class RecordScreenshotTest < SystemTestCase setup do - screenshot_section class_name.underscore.sub(/(_feature|_system)?_test$/, "") unless CapybaraScreenshotDiff.screenshot_namer.section - screenshot_group name[5..] unless CapybaraScreenshotDiff.screenshot_namer.group + screenshot_section class_name.underscore.sub(/(_feature|_system)?_test$/, "") unless SnapDiff.session.screenshot_namer.section + screenshot_group name[5..] unless SnapDiff.session.screenshot_namer.group - @original_tolerance = Capybara::Screenshot::Diff.tolerance - Capybara::Screenshot::Diff.tolerance = (Capybara::Screenshot::Diff.driver == :vips) ? 0.035 : 0.7 + @original_tolerance = SnapDiff.config.tolerance + SnapDiff.config.tolerance = (SnapDiff.config.driver == :vips) ? 0.035 : 0.7 end teardown do - Capybara::Screenshot.blur_active_element = nil - Capybara::Screenshot::Diff.tolerance = @original_tolerance + SnapDiff.config.blur_active_element = nil + SnapDiff.config.tolerance = @original_tolerance end def test_record_index diff --git a/test/integration/rspec_after_hook_order_masking_test.rb b/test/integration/rspec_after_hook_order_masking_test.rb index ac890878..2a8a8f61 100644 --- a/test/integration/rspec_after_hook_order_masking_test.rb +++ b/test/integration/rspec_after_hook_order_masking_test.rb @@ -5,44 +5,42 @@ require "json" require "tmpdir" -module CapybaraScreenshotDiff - # Regression test for the "gem's pending hook must run after the full user - # after-chain" guard in capybara_screenshot_diff/rspec.rb. - # - # This must run the fixture in a genuinely separate process: RSpec's - # `after(:each)` hooks run in REVERSE registration order, so a - # `config.after` hook registered BEFORE `capybara_screenshot_diff/rspec` - # is required runs AFTER the gem's own `config.after` hook. If the gem's - # hook isn't guaranteed to run last, it commits to "pending" before the - # user's hook has had a chance to raise, and that later raise gets folded - # into `pending_exception` (via `Example#set_exception`) rather than - # `example.exception`, silently masking the real failure. That only - # reproduces end-to-end via a real `RSpec::Core::Runner.run` exit status + - # report, not via an in-process assertion on some Ruby object. - class RspecAfterHookOrderMaskingTest < ActiveSupport::TestCase - test "a real failure from an after hook registered before the gem loaded is never masked as pending" do - spec_file = file_fixture("rspec_after_hook_order_masking_spec.rb").to_s - - Dir.mktmpdir do |dir| - json_path = File.join(dir, "result.json") - - script = <<~RUBY - require "rspec/core" - exit RSpec::Core::Runner.run([#{spec_file.inspect}, "--format", "json", "--out", #{json_path.inspect}], $stderr, $stdout) - RUBY - - out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-Itest", "-e", script) - - refute status.success?, "expected the after-hook-order-masking fixture to fail the process, got:\n#{out}" - - summary = JSON.parse(File.read(json_path))["summary"] - - assert_equal 1, summary["example_count"], out - assert_equal 1, summary["failure_count"], - "expected the after hook's real failure to be reported as a failure, not masked:\n#{out}" - assert_equal 0, summary["pending_count"], - "expected pending_if_new to NOT mask the after hook's failure as pending:\n#{out}" - end +# Regression test for the "gem's pending hook must run after the full user +# after-chain" guard in capybara_screenshot_diff/rspec.rb. +# +# This must run the fixture in a genuinely separate process: RSpec's +# `after(:each)` hooks run in REVERSE registration order, so a +# `config.after` hook registered BEFORE `capybara_screenshot_diff/rspec` +# is required runs AFTER the gem's own `config.after` hook. If the gem's +# hook isn't guaranteed to run last, it commits to "pending" before the +# user's hook has had a chance to raise, and that later raise gets folded +# into `pending_exception` (via `Example#set_exception`) rather than +# `example.exception`, silently masking the real failure. That only +# reproduces end-to-end via a real `RSpec::Core::Runner.run` exit status + +# report, not via an in-process assertion on some Ruby object. +class RspecAfterHookOrderMaskingTest < ActiveSupport::TestCase + test "a real failure from an after hook registered before the gem loaded is never masked as pending" do + spec_file = file_fixture("rspec_after_hook_order_masking_spec.rb").to_s + + Dir.mktmpdir do |dir| + json_path = File.join(dir, "result.json") + + script = <<~RUBY + require "rspec/core" + exit RSpec::Core::Runner.run([#{spec_file.inspect}, "--format", "json", "--out", #{json_path.inspect}], $stderr, $stdout) + RUBY + + out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-Itest", "-e", script) + + refute status.success?, "expected the after-hook-order-masking fixture to fail the process, got:\n#{out}" + + summary = JSON.parse(File.read(json_path))["summary"] + + assert_equal 1, summary["example_count"], out + assert_equal 1, summary["failure_count"], + "expected the after hook's real failure to be reported as a failure, not masked:\n#{out}" + assert_equal 0, summary["pending_count"], + "expected pending_if_new to NOT mask the after hook's failure as pending:\n#{out}" end end end diff --git a/test/integration/rspec_pending_masking_test.rb b/test/integration/rspec_pending_masking_test.rb index 038b3720..32369363 100644 --- a/test/integration/rspec_pending_masking_test.rb +++ b/test/integration/rspec_pending_masking_test.rb @@ -5,41 +5,39 @@ require "json" require "tmpdir" -module CapybaraScreenshotDiff - # Regression test for the "never mask a real failure with a pending - # marker" guard in capybara_screenshot_diff/rspec.rb's `config.after` hook. - # - # This must run the fixture in a genuinely separate process: RSpec's own - # after-hook exception handling folds a `skip` raised from an after-hook - # into the example's pending state (it resets `example.exception` because - # `Pending.mark_skipped!` already flipped `example.pending?` to true before - # the skip exception is caught). That only reproduces end-to-end via a - # real `RSpec::Core::Runner.run` exit status + report, not via an - # in-process assertion on some Ruby object. - class RspecPendingMaskingTest < ActiveSupport::TestCase - test "a genuine example failure is never masked as pending by pending_if_new" do - spec_file = file_fixture("rspec_pending_masking_spec.rb").to_s - - Dir.mktmpdir do |dir| - json_path = File.join(dir, "result.json") - - script = <<~RUBY - require "rspec/core" - exit RSpec::Core::Runner.run([#{spec_file.inspect}, "--format", "json", "--out", #{json_path.inspect}], $stderr, $stdout) - RUBY - - out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-Itest", "-e", script) - - refute status.success?, "expected the pending-masking fixture to fail the process, got:\n#{out}" - - summary = JSON.parse(File.read(json_path))["summary"] - - assert_equal 1, summary["example_count"], out - assert_equal 1, summary["failure_count"], - "expected the deliberate failure to be reported as a failure, not masked:\n#{out}" - assert_equal 0, summary["pending_count"], - "expected pending_if_new to NOT mask the failure as pending:\n#{out}" - end +# Regression test for the "never mask a real failure with a pending +# marker" guard in capybara_screenshot_diff/rspec.rb's `config.after` hook. +# +# This must run the fixture in a genuinely separate process: RSpec's own +# after-hook exception handling folds a `skip` raised from an after-hook +# into the example's pending state (it resets `example.exception` because +# `Pending.mark_skipped!` already flipped `example.pending?` to true before +# the skip exception is caught). That only reproduces end-to-end via a +# real `RSpec::Core::Runner.run` exit status + report, not via an +# in-process assertion on some Ruby object. +class RspecPendingMaskingTest < ActiveSupport::TestCase + test "a genuine example failure is never masked as pending by pending_if_new" do + spec_file = file_fixture("rspec_pending_masking_spec.rb").to_s + + Dir.mktmpdir do |dir| + json_path = File.join(dir, "result.json") + + script = <<~RUBY + require "rspec/core" + exit RSpec::Core::Runner.run([#{spec_file.inspect}, "--format", "json", "--out", #{json_path.inspect}], $stderr, $stdout) + RUBY + + out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-Itest", "-e", script) + + refute status.success?, "expected the pending-masking fixture to fail the process, got:\n#{out}" + + summary = JSON.parse(File.read(json_path))["summary"] + + assert_equal 1, summary["example_count"], out + assert_equal 1, summary["failure_count"], + "expected the deliberate failure to be reported as a failure, not masked:\n#{out}" + assert_equal 0, summary["pending_count"], + "expected pending_if_new to NOT mask the failure as pending:\n#{out}" end end end diff --git a/test/integration/rspec_test.rb b/test/integration/rspec_test.rb index 1e772401..c51e2f56 100644 --- a/test/integration/rspec_test.rb +++ b/test/integration/rspec_test.rb @@ -2,18 +2,16 @@ require "system_test_case" -module CapybaraScreenshotDiff - class RspecTest < SystemTestCase - test "RSpec integration runs successfully with capybara-screenshot-diff" do - # Ensure that the RSpec module is loaded - require "rspec/core" +class RspecTest < SystemTestCase + test "RSpec integration runs successfully with capybara-screenshot-diff" do + # Ensure that the RSpec module is loaded + require "rspec/core" - # Run the RSpec spec file - capture_output = StringIO.new - spec_file = file_fixture("rspec_spec.rb").to_s - rspec_status = RSpec::Core::Runner.run([spec_file], capture_output, capture_output) + # Run the RSpec spec file + capture_output = StringIO.new + spec_file = file_fixture("rspec_spec.rb").to_s + rspec_status = RSpec::Core::Runner.run([spec_file], capture_output, capture_output) - assert_equal 0, rspec_status, "RSpec tests failed:\n#{capture_output.string}" - end + assert_equal 0, rspec_status, "RSpec tests failed:\n#{capture_output.string}" end end diff --git a/test/integration/test_methods_system_test.rb b/test/integration/test_methods_system_test.rb index 5aae815e..2f652999 100644 --- a/test/integration/test_methods_system_test.rb +++ b/test/integration/test_methods_system_test.rb @@ -11,27 +11,21 @@ require "action_pack/version" require "objspace" -module Capybara - module Screenshot - module Diff - class TestMethodsSystemTest < ActionDispatch::SystemTestCase - include SnapDiff::DSL - include CapybaraScreenshotDiff::DSLStub +class TestMethodsSystemTest < ActionDispatch::SystemTestCase + include SnapDiff::DSL + include DSLStub - driven_by :selenium, using: :headless_chrome + driven_by :selenium, using: :headless_chrome - def test_current_capybara_driver_class_do_not_spawn_new_process_when_we_use_system_test_cases - # NOTE: There is possible that we have several drivers usage in the one suite, - # so each of them will have separate instance - other_activated_drivers = ObjectSpace.each_object(Capybara::Selenium::Driver).count + def test_current_capybara_driver_class_do_not_spawn_new_process_when_we_use_system_test_cases + # NOTE: There is possible that we have several drivers usage in the one suite, + # so each of them will have separate instance + other_activated_drivers = ObjectSpace.each_object(Capybara::Selenium::Driver).count - 3.times { SnapDiff::BrowserHelpers.current_capybara_driver_class } + 3.times { SnapDiff::BrowserHelpers.current_capybara_driver_class } - run_chrome_drivers = ObjectSpace.each_object(Capybara::Selenium::Driver).count - assert run_chrome_drivers.positive? - assert run_chrome_drivers - other_activated_drivers <= 1 - end - end - end + run_chrome_drivers = ObjectSpace.each_object(Capybara::Selenium::Driver).count + assert run_chrome_drivers.positive? + assert run_chrome_drivers - other_activated_drivers <= 1 end end diff --git a/test/legacy/legacy_forwarders_test.rb b/test/legacy/legacy_forwarders_test.rb new file mode 100644 index 00000000..5ed3b3e7 --- /dev/null +++ b/test/legacy/legacy_forwarders_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "test_helper" +# The shared harness loads canonical entry points only, so a legacy-surface +# test pulls in the v1 entry itself -- the require goes with the file in 3.0. +require "capybara_screenshot_diff" +require "capybara_screenshot_diff/static" + +# LEGACY SURFACE (test/legacy/, see the Rakefile). +# +# The identity claims that make the old CapybaraScreenshotDiff module a +# *view* of the canonical state rather than a second copy of it, plus the +# v1-shaped SnapDiff.start. Collected here from the canonical tests they +# used to sit in (registry_concurrency_test, reporters_mutex_test, +# snap_diff_test): a mechanical repoint would have turned each of them into +# `assert_same X, X`, which is how a real claim quietly becomes a tautology. +# Verbatim, so the v1 contract keeps exactly the coverage it had. +class LegacyForwardersTest < ActiveSupport::TestCase + setup do + # These resolve old-namespace names on purpose; the suite-wide guard in + # test_helper raises on unexpected shim warnings. + @original_silence = SnapDiff.silence_deprecations + SnapDiff.silence_deprecations = true + end + + teardown do + SnapDiff.silence_deprecations = @original_silence + end + + # ADR-008 step 6: SnapDiff.session is the canonical accessor and + # CapybaraScreenshotDiff.registry a forwarder over it -- they must hand + # back the *same* object, not two registries that happen to look alike. + test "SnapDiff.session and CapybaraScreenshotDiff.registry are the same object" do + assert_same SnapDiff.session, CapybaraScreenshotDiff.registry + + SnapDiff.session.record_new_screenshot("shared_object_probe") + assert_equal ["shared_object_probe"], CapybaraScreenshotDiff.new_screenshots + ensure + SnapDiff.session.reset + end + + # ADR-008 step 6: SnapDiff::Reporting.register is the canonical way in; + # CapybaraScreenshotDiff.reporters stays as the compat view of the same + # array, so a registration must be visible through both. + test "register appends to the array CapybaraScreenshotDiff.reporters exposes" do + original_reporters = CapybaraScreenshotDiff.reporters.dup + CapybaraScreenshotDiff.reporters.clear + reporter = Object.new + + assert_same reporter, SnapDiff::Reporting.register(reporter) + assert_same SnapDiff::Reporting.reporters, CapybaraScreenshotDiff.reporters + assert_includes CapybaraScreenshotDiff.reporters, reporter + ensure + CapybaraScreenshotDiff.reporters.clear + CapybaraScreenshotDiff.reporters.concat(original_reporters) + end + + test "CapybaraScreenshotDiff.reporters_mutex is the canonical Reporting mutex" do + assert_same SnapDiff::Reporting.mutex, CapybaraScreenshotDiff.reporters_mutex + end + + test "Capybara::Screenshot::Diff::ImageCompare aliases SnapDiff::Comparison" do + assert_same SnapDiff::Comparison, Capybara::Screenshot::Diff::ImageCompare + end + + test "CapybaraScreenshotDiff.serve forwards to SnapDiff.serve, custom root included" do + original_root = SnapDiff.config.root + + CapybaraScreenshotDiff.serve("test/fixtures", root: "/tmp") + + assert_equal Pathname("/tmp"), SnapDiff.config.root + ensure + Capybara.app = Rails.application + SnapDiff.config.root = original_root + end + + test ".start yields the same objects Diff.configure yields" do + yielded = [] + Capybara::Screenshot::Diff.configure { |screenshot, diff| yielded << [screenshot, diff] } + + started = [] + SnapDiff.start { |screenshot, diff| started << [screenshot, diff] } + + assert_equal yielded, started + end + + test ".start applies a setting like Diff.configure does" do + original = SnapDiff.config.tolerance + + begin + SnapDiff.start { |_screenshot, diff| diff.tolerance = 0.0123 } + + assert_equal 0.0123, SnapDiff.config.tolerance + ensure + SnapDiff.config.tolerance = original + end + end +end diff --git a/test/support/capybara_screenshot_diff/dsl_stub.rb b/test/support/capybara_screenshot_diff/dsl_stub.rb deleted file mode 100644 index d9749edb..00000000 --- a/test/support/capybara_screenshot_diff/dsl_stub.rb +++ /dev/null @@ -1,65 +0,0 @@ -require "active_support/concern" - -module CapybaraScreenshotDiff - module DSLStub - extend ActiveSupport::Concern - - def setup - super - @manager = SnapDiff::SnapManager.new(Capybara::Screenshot.root / "doc/screenshots") - Capybara::Screenshot::Diff.screenshoter = Capybara::Screenshot::ScreenshoterStub - end - - def teardown - @manager.cleanup! - Capybara::Screenshot::Diff.screenshoter = SnapDiff::Screenshoter - CapybaraScreenshotDiff.reset - super - end - - # Prepare comparison images and build ImageCompare for them - def make_comparison(fixture_base_image, fixture_new_image = nil, destination: "screenshot", **options) - fixture_new_image ||= fixture_base_image - snap = create_snapshot_for(fixture_base_image, fixture_new_image, name: destination) - SnapDiff::Comparison.new(snap.path, snap.base_path, **options) - end - - # Prepare images for comparison in a test - # - # @param snap [SnapDiff::Snap] the snapshot to prepare - # @param expected [String] the base name of the original base image - # @param actual [String] the base name of the original new image - def set_test_images(snap, expected, actual) - @manager.provision_snap_with(snap, fixture_image_path_from(actual, snap.format), version: :actual) - @manager.provision_snap_with(snap, fixture_image_path_from(expected, snap.format), version: :base) - end - - ImageCompareStub = Struct.new( - :driver, :driver_options, :shift_distance_limit, :quick_equal?, :different?, :reporter, keyword_init: true - ) - - def build_image_compare_stub(equal: true) - ImageCompareStub.new( - driver: ::Minitest::Mock.new, - reporter: ::Minitest::Mock.new, - driver_options: Capybara::Screenshot::Diff.default_options, - shift_distance_limit: nil, - quick_equal?: equal, - different?: !equal - ) - end - - def take_stable_screenshot_with(snap, stability_time_limit: 0.01, wait: 10) - screenshoter = SnapDiff::StableScreenshoter.new({stability_time_limit: stability_time_limit, wait: wait}) - screenshoter.take_stable_screenshot(snap) - end - - def create_snapshot_for(expected, actual = nil, name: nil) - actual ||= expected - name ||= "#{actual}_#{Time.now.nsec}" - @manager.snapshot(name).tap do |snap| - set_test_images(snap, expected, actual) - end - end - end -end diff --git a/test/support/driver_coverage.rb b/test/support/driver_coverage.rb index 82246cea..b6e430d4 100644 --- a/test/support/driver_coverage.rb +++ b/test/support/driver_coverage.rb @@ -1,32 +1,33 @@ # frozen_string_literal: true -module CapybaraScreenshotDiff - # Reports which screenshot-diff drivers were detected as loadable for this run, and - # guards CI against a driver going silently missing (e.g. libvips not installed). - # - # Driver availability is detected once, at load time, via - # Capybara::Screenshot::Diff::AVAILABLE_DRIVERS — so if libvips is missing, - # vips-gated tests (see test/unit/drivers/vips_driver_test.rb) register and - # report as skips rather than failing. That's expected on a vips-less runner; - # this module exists so CI specifically (not a plain dev machine) still fails - # loudly when a driver it's supposed to have goes missing. - module DriverCoverage - ALL_DRIVERS = %i[chunky_png vips].freeze +# Reports which screenshot-diff drivers were detected as loadable for this run, and +# guards CI against a driver going silently missing (e.g. libvips not installed). +# +# Driver availability is detected once, at load time, via +# SnapDiff::Drivers.available — so if libvips is missing, vips-gated tests +# (see test/unit/drivers/vips_driver_test.rb) register and report as skips +# rather than failing. That's expected on a vips-less runner; this module +# exists so CI specifically (not a plain dev machine) still fails loudly when +# a driver it's supposed to have goes missing. +# +# Plain top-level module: it is test scaffolding, so it has no business +# reopening a gem namespace -- least of all the v1 one 3.0 deletes. +module DriverCoverage + ALL_DRIVERS = %i[chunky_png vips].freeze - def self.banner(available) - unavailable = ALL_DRIVERS - available - msg = "[capybara-screenshot-diff] drivers detected: #{available.join(", ")}" - msg += " | unavailable: #{unavailable.join(", ")}" if unavailable.any? - msg - end + def self.banner(available) + unavailable = ALL_DRIVERS - available + msg = "[capybara-screenshot-diff] drivers detected: #{available.join(", ")}" + msg += " | unavailable: #{unavailable.join(", ")}" if unavailable.any? + msg + end - # Drivers CI is missing but expected to have. Empty outside CI, or when an - # expected driver was explicitly excluded (e.g. a runner that can't install - # libvips) via the `exclude` list. - def self.missing_for_ci(available, ci:, exclude: []) - return [] if ci.to_s.empty? + # Drivers CI is missing but expected to have. Empty outside CI, or when an + # expected driver was explicitly excluded (e.g. a runner that can't install + # libvips) via the `exclude` list. + def self.missing_for_ci(available, ci:, exclude: []) + return [] if ci.to_s.empty? - (ALL_DRIVERS - Array(exclude)) - available - end + (ALL_DRIVERS - Array(exclude)) - available end end diff --git a/test/support/dsl_stub.rb b/test/support/dsl_stub.rb new file mode 100644 index 00000000..5b7db45a --- /dev/null +++ b/test/support/dsl_stub.rb @@ -0,0 +1,65 @@ +require "active_support/concern" + +# Plain top-level module: test scaffolding has no business reopening a gem +# namespace -- least of all the v1 one 3.0 deletes. +module DSLStub + extend ActiveSupport::Concern + + def setup + super + @manager = SnapDiff::SnapManager.new(SnapDiff.config.root / "doc/screenshots") + SnapDiff.config.screenshoter = ScreenshoterStub + end + + def teardown + @manager.cleanup! + SnapDiff.config.screenshoter = SnapDiff::Screenshoter + SnapDiff.reset + super + end + + # Prepare comparison images and build a Comparison for them + def make_comparison(fixture_base_image, fixture_new_image = nil, destination: "screenshot", **options) + fixture_new_image ||= fixture_base_image + snap = create_snapshot_for(fixture_base_image, fixture_new_image, name: destination) + SnapDiff::Comparison.new(snap.path, snap.base_path, **options) + end + + # Prepare images for comparison in a test + # + # @param snap [SnapDiff::Snap] the snapshot to prepare + # @param expected [String] the base name of the original base image + # @param actual [String] the base name of the original new image + def set_test_images(snap, expected, actual) + @manager.provision_snap_with(snap, fixture_image_path_from(actual, snap.format), version: :actual) + @manager.provision_snap_with(snap, fixture_image_path_from(expected, snap.format), version: :base) + end + + ImageCompareStub = Struct.new( + :driver, :driver_options, :shift_distance_limit, :quick_equal?, :different?, :reporter, keyword_init: true + ) + + def build_image_compare_stub(equal: true) + ImageCompareStub.new( + driver: ::Minitest::Mock.new, + reporter: ::Minitest::Mock.new, + driver_options: SnapDiff.config.default_options, + shift_distance_limit: nil, + quick_equal?: equal, + different?: !equal + ) + end + + def take_stable_screenshot_with(snap, stability_time_limit: 0.01, wait: 10) + screenshoter = SnapDiff::StableScreenshoter.new({stability_time_limit: stability_time_limit, wait: wait}) + screenshoter.take_stable_screenshot(snap) + end + + def create_snapshot_for(expected, actual = nil, name: nil) + actual ||= expected + name ||= "#{actual}_#{Time.now.nsec}" + @manager.snapshot(name).tap do |snap| + set_test_images(snap, expected, actual) + end + end +end diff --git a/test/support/non_minitest_assertions.rb b/test/support/non_minitest_assertions.rb index 6d8ccbca..866faabe 100644 --- a/test/support/non_minitest_assertions.rb +++ b/test/support/non_minitest_assertions.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true -require "capybara_screenshot_diff" +require "snap_diff/dsl" -module CapybaraScreenshotDiff - module NonMinitest - module Assertions - def self.included(klass) - klass.include SnapDiff::DSL +# Stands in for a non-minitest adapter: the raw session lifecycle a host +# framework's integration has to drive itself. +module NonMinitest + module Assertions + def self.included(klass) + klass.include SnapDiff::DSL - klass.setup do - SnapDiff::BrowserHelpers.resize_window_if_needed - end + klass.setup do + SnapDiff::BrowserHelpers.resize_window_if_needed + end - klass.teardown do - CapybaraScreenshotDiff.verify - ensure - CapybaraScreenshotDiff.reset - end + klass.teardown do + SnapDiff.session.verify + ensure + SnapDiff.reset end end end diff --git a/test/support/screenshoter_stub.rb b/test/support/screenshoter_stub.rb index 87d4464e..b0ce4333 100644 --- a/test/support/screenshoter_stub.rb +++ b/test/support/screenshoter_stub.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "capybara/screenshot/diff/screenshoter" +require "snap_diff/screenshoter" -class Capybara::Screenshot::ScreenshoterStub < SnapDiff::Screenshoter +class ScreenshoterStub < SnapDiff::Screenshoter def pending_image_to_load end diff --git a/test/support/setup_capybara_drivers.rb b/test/support/setup_capybara_drivers.rb index b679cd07..46dfb17c 100644 --- a/test/support/setup_capybara_drivers.rb +++ b/test/support/setup_capybara_drivers.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "capybara/screenshot/diff/os" +require "snap_diff/os" ENV["CAPYBARA_DRIVER"] ||= "cuprite" SCREEN_SIZE = [800, 600] -if ENV["CAPYBARA_DRIVER"] == "selenium_chrome_headless" && Capybara::Screenshot::Os.name == "linux" +if ENV["CAPYBARA_DRIVER"] == "selenium_chrome_headless" && SnapDiff::Os.name == "linux" SCREEN_SIZE[1] += 87 # Add extra space for address field etc. end BROWSERS = {cuprite: "chrome", selenium_headless: "firefox", selenium_chrome_headless: "chrome"} diff --git a/test/support/stub_test_methods.rb b/test/support/stub_test_methods.rb index ad1575bf..bc4d79ac 100644 --- a/test/support/stub_test_methods.rb +++ b/test/support/stub_test_methods.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true require_relative "screenshoter_stub" -require_relative "capybara_screenshot_diff/dsl_stub" +require_relative "dsl_stub" diff --git a/test/support/test_doubles.rb b/test/support/test_doubles.rb index 989e7a0d..3c60d4cc 100644 --- a/test/support/test_doubles.rb +++ b/test/support/test_doubles.rb @@ -1,129 +1,123 @@ # frozen_string_literal: true -module Capybara - module Screenshot - module Diff - module TestDoubles - # Test double for file paths with configurable size and existence - class TestPath - attr_reader :size_value - - # Initialize a path with a size value and existence flag - # @param size_value [Integer] The size of the file - # @param exists [Boolean] Whether the file exists, defaults to true - def initialize(size_value, exists = true) - @size_value = size_value - @exists = exists - end - - def size - @size_value - end - - def exist? - @exists - end - end - - # Test double for image drivers with configurable behavior - class TestDriver - attr_reader :add_black_box_calls, :filter_calls, :dimension_check_calls, :pixel_check_calls, :difference_region_calls, :load_images_called, :load_images_args - attr_accessor :same_dimension_result, :same_pixels_result, :difference_region_result, :images_to_return - - # Initializes a new TestDriver - # @param is_vips_driver [Boolean] whether this driver should behave like a VipsDriver - # @param images_to_return [Array] images to return from load_images method - def initialize(is_vips_driver = false, images_to_return = nil) - @is_vips_driver = is_vips_driver - @images_to_return = images_to_return || [:base_image, :new_image] - @add_black_box_calls = [] - @filter_calls = [] - @dimension_check_calls = [] - @pixel_check_calls = [] - @difference_region_calls = [] - @load_images_called = false - @load_images_args = nil - @same_dimension_result = true - @same_pixels_result = true - @difference_region_result = nil - end - - def is_a?(klass) - return @is_vips_driver if klass == SnapDiff::Drivers::VipsDriver - super - end - - def add_black_box(image, region) - @add_black_box_calls << {image: image, region: region} - "processed_#{image}" - end - - def filter_image_with_median(image, size) - @filter_calls << {image: image, size: size} - # Return the filtered image, converting to the expected format - "filtered_#{image}" - end - - def same_dimension?(comparison) - @dimension_check_calls << comparison - @same_dimension_result - end - - def same_pixels?(comparison) - @pixel_check_calls << comparison - @same_pixels_result - end - - def find_difference_region(comparison) - @difference_region_calls << comparison - @difference_region_result - end - - def load_images(base_path, new_path) - @load_images_called = true - @load_images_args = [base_path, new_path] - @images_to_return - end - - def supports?(...) - @is_vips_driver - end - - # Returns Object so warning messages in ImagePreprocessor don't - # couple tests to the TestDriver class name. - def class - Object - end - end - - # Test double for difference results - class TestDifference - attr_reader :different_value - - def initialize(different_value) - @different_value = different_value - end - - def different? - @different_value - end - end - - # Simple test double for comparison objects - class TestComparison - attr_reader :new_image, :base_image, :options, :driver - attr_accessor :new_image_path, :base_image_path - - def initialize(options = {}) - @new_image = options[:new_image] - @base_image = options[:base_image] - @options = options[:options] || {} - @driver = options[:driver] - @new_image_path = options[:new_image_path] || options[:image_path] - @base_image_path = options[:base_image_path] - end - end - end +module TestDoubles + # Test double for file paths with configurable size and existence + class TestPath + attr_reader :size_value + + # Initialize a path with a size value and existence flag + # @param size_value [Integer] The size of the file + # @param exists [Boolean] Whether the file exists, defaults to true + def initialize(size_value, exists = true) + @size_value = size_value + @exists = exists + end + + def size + @size_value + end + + def exist? + @exists + end + end + + # Test double for image drivers with configurable behavior + class TestDriver + attr_reader :add_black_box_calls, :filter_calls, :dimension_check_calls, :pixel_check_calls, :difference_region_calls, :load_images_called, :load_images_args + attr_accessor :same_dimension_result, :same_pixels_result, :difference_region_result, :images_to_return + + # Initializes a new TestDriver + # @param is_vips_driver [Boolean] whether this driver should behave like a VipsDriver + # @param images_to_return [Array] images to return from load_images method + def initialize(is_vips_driver = false, images_to_return = nil) + @is_vips_driver = is_vips_driver + @images_to_return = images_to_return || [:base_image, :new_image] + @add_black_box_calls = [] + @filter_calls = [] + @dimension_check_calls = [] + @pixel_check_calls = [] + @difference_region_calls = [] + @load_images_called = false + @load_images_args = nil + @same_dimension_result = true + @same_pixels_result = true + @difference_region_result = nil + end + + def is_a?(klass) + return @is_vips_driver if klass == SnapDiff::Drivers::VipsDriver + super + end + + def add_black_box(image, region) + @add_black_box_calls << {image: image, region: region} + "processed_#{image}" + end + + def filter_image_with_median(image, size) + @filter_calls << {image: image, size: size} + # Return the filtered image, converting to the expected format + "filtered_#{image}" + end + + def same_dimension?(comparison) + @dimension_check_calls << comparison + @same_dimension_result + end + + def same_pixels?(comparison) + @pixel_check_calls << comparison + @same_pixels_result + end + + def find_difference_region(comparison) + @difference_region_calls << comparison + @difference_region_result + end + + def load_images(base_path, new_path) + @load_images_called = true + @load_images_args = [base_path, new_path] + @images_to_return + end + + def supports?(...) + @is_vips_driver + end + + # Returns Object so warning messages in ImagePreprocessor don't + # couple tests to the TestDriver class name. + def class + Object + end + end + + # Test double for difference results + class TestDifference + attr_reader :different_value + + def initialize(different_value) + @different_value = different_value + end + + def different? + @different_value + end + end + + # Simple test double for comparison objects + class TestComparison + attr_reader :new_image, :base_image, :options, :driver + attr_accessor :new_image_path, :base_image_path + + def initialize(options = {}) + @new_image = options[:new_image] + @base_image = options[:base_image] + @options = options[:options] || {} + @driver = options[:driver] + @new_image_path = options[:new_image_path] || options[:image_path] + @base_image_path = options[:base_image_path] end end end diff --git a/test/support/test_helpers.rb b/test/support/test_helpers.rb index e0a41c12..03d47f8f 100644 --- a/test/support/test_helpers.rb +++ b/test/support/test_helpers.rb @@ -3,7 +3,7 @@ require "support/test_doubles" module TestHelpers - include Capybara::Screenshot::Diff::TestDoubles + include TestDoubles # Common assertions for image comparison tests module Assertions @@ -44,7 +44,7 @@ module TestData # @param images [Array, nil] Images to return from load_images (default: nil) # @return [TestDoubles::TestDriver] A test driver object def create_test_driver(is_vips: false, images: nil) - Capybara::Screenshot::Diff::TestDoubles::TestDriver.new(is_vips, images) + TestDoubles::TestDriver.new(is_vips, images) end end end diff --git a/test/system_test_case.rb b/test/system_test_case.rb index 02f2c221..e2e419e1 100644 --- a/test/system_test_case.rb +++ b/test/system_test_case.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff/minitest" -require "capybara_screenshot_diff/reporters/html" +require "snap_diff/integrations/minitest" +require "snap_diff/reporters/html" require "support/setup_capybara_drivers" @@ -11,47 +11,47 @@ class SystemTestCase < ActiveSupport::TestCase Capybara.current_driver = Capybara.javascript_driver Capybara.page.current_window.resize_to(*SCREEN_SIZE) - Capybara::Screenshot.enabled = true - Capybara::Screenshot::Diff.enabled = true + SnapDiff.config.screenshot_enabled = true + SnapDiff.config.enabled = true # TODO: Reset original settings to previous values - @orig_root = Capybara::Screenshot.root - Capybara::Screenshot.root = Rails.root / "../test/fixtures/app" + @orig_root = SnapDiff.config.root + SnapDiff.config.root = Rails.root / "../test/fixtures/app" - @orig_save_path = Capybara::Screenshot.save_path - Capybara::Screenshot.save_path = "./doc/screenshots" + @orig_save_path = SnapDiff.config.save_path + SnapDiff.config.save_path = "./doc/screenshots" - Capybara::Screenshot::Diff.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym + SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym # TODO: Makes configurations copying and restoring much easier - @orig_add_os_path = Capybara::Screenshot.add_os_path - Capybara::Screenshot.add_os_path = true - @orig_add_driver_path = Capybara::Screenshot.add_driver_path - Capybara::Screenshot.add_driver_path = true - # NOTE: Only works before `include Capybara::Screenshot::Diff` line - @orig_window_size = Capybara::Screenshot.window_size - Capybara::Screenshot.window_size = SCREEN_SIZE + @orig_add_os_path = SnapDiff.config.add_os_path + SnapDiff.config.add_os_path = true + @orig_add_driver_path = SnapDiff.config.add_driver_path + SnapDiff.config.add_driver_path = true + # NOTE: Only works before the `include SnapDiff::DSL` line + @orig_window_size = SnapDiff.config.window_size + SnapDiff.config.window_size = SCREEN_SIZE # NOTE: For small screenshots we should have pixel perfect comparisons - @orig_tolerance = Capybara::Screenshot::Diff.tolerance - Capybara::Screenshot::Diff.tolerance = nil + @orig_tolerance = SnapDiff.config.tolerance + SnapDiff.config.tolerance = nil end - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::Minitest::Assertions + include SnapDiff::DSL + include SnapDiff::Minitest::Assertions teardown do # Restore to previous values - Capybara::Screenshot.root = @orig_root - Capybara::Screenshot.save_path = @orig_save_path - Capybara::Screenshot.add_os_path = @orig_add_os_path - Capybara::Screenshot.add_driver_path = @orig_add_driver_path - Capybara::Screenshot.window_size = @orig_window_size - Capybara::Screenshot::Diff.tolerance = @orig_tolerance + SnapDiff.config.root = @orig_root + SnapDiff.config.save_path = @orig_save_path + SnapDiff.config.add_os_path = @orig_add_os_path + SnapDiff.config.add_driver_path = @orig_add_driver_path + SnapDiff.config.window_size = @orig_window_size + SnapDiff.config.tolerance = @orig_tolerance Capybara.current_driver = Capybara.default_driver - if Capybara::Screenshot::Diff.driver == :vips + if SnapDiff.config.driver == :vips Vips.cache_set_max(0) Vips.cache_set_max(1000) end @@ -66,7 +66,7 @@ def rollback_comparison_runtime_files(screenshot_assert) save_annotations_for_debug(comparison) screenshot_path = comparison.image_path - SnapDiff::Vcs.checkout_vcs(Capybara::Screenshot.root, screenshot_path, screenshot_path) + SnapDiff::Vcs.checkout_vcs(SnapDiff.config.root, screenshot_path, screenshot_path) if comparison.difference comparison.reporter.clean_tmp_files diff --git a/test/test_helper.rb b/test/test_helper.rb index 0484461d..722aeb12 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,7 +22,7 @@ require "capybara/minitest" require "support/setup_capybara" -require "capybara_screenshot_diff/minitest" +require "snap_diff/integrations/minitest" # v2 step 8: the suite exercises only canonical SnapDiff:: names, so any # legacy-shim deprecation warning during a test run is a bug in the @@ -51,13 +51,13 @@ def warn(message, ...) require "support/test_helpers" require "support/driver_coverage" -Capybara::Screenshot.root = Rails.root -Capybara::Screenshot.save_path = "./doc/screenshots" +SnapDiff.config.root = Rails.root +SnapDiff.config.save_path = "./doc/screenshots" -puts CapybaraScreenshotDiff::DriverCoverage.banner(Capybara::Screenshot::Diff::AVAILABLE_DRIVERS) +puts DriverCoverage.banner(SnapDiff::Drivers.available) -missing_drivers = CapybaraScreenshotDiff::DriverCoverage.missing_for_ci( - Capybara::Screenshot::Diff::AVAILABLE_DRIVERS, +missing_drivers = DriverCoverage.missing_for_ci( + SnapDiff::Drivers.available, ci: ENV["CI"], exclude: ENV["CI_EXPECTED_DRIVERS_EXCLUDE"]&.split(",")&.map(&:to_sym) ) @@ -84,10 +84,10 @@ class ActiveSupport::TestCase @_orig_cwd = Dir.pwd @_orig_capybara_app = Capybara.app - Capybara::Screenshot::Diff.fail_if_new = false - Capybara::Screenshot.blur_active_element = false - Capybara::Screenshot.hide_caret = false - Capybara::Screenshot.disable_animations = false + SnapDiff.config.fail_if_new = false + SnapDiff.config.blur_active_element = false + SnapDiff.config.hide_caret = false + SnapDiff.config.disable_animations = false end teardown do diff --git a/test/unit/annotation_service_test.rb b/test/unit/annotation_service_test.rb index 096a9614..a25dabc2 100644 --- a/test/unit/annotation_service_test.rb +++ b/test/unit/annotation_service_test.rb @@ -1,78 +1,76 @@ # frozen_string_literal: true require "test_helper" -require "capybara/screenshot/diff/annotation_service" +require "snap_diff/annotation_service" unless defined?(Vips) warn "VIPS not present. Skipping VIPS driver tests." return end -require "capybara/screenshot/diff/drivers/vips_driver" +require "snap_diff/drivers/vips_driver" -module Capybara::Screenshot::Diff - class AnnotationServiceTest < ActiveSupport::TestCase - setup do - @_tmpdir = Pathname.new(Dir.mktmpdir) - end +class AnnotationServiceTest < ActiveSupport::TestCase + setup do + @_tmpdir = Pathname.new(Dir.mktmpdir) + end - teardown do - FileUtils.remove_entry @_tmpdir if @_tmpdir - end + teardown do + FileUtils.remove_entry @_tmpdir if @_tmpdir + end - test "#annotate_and_save_images writes annotated and heatmap images" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - driver = SnapDiff::Drivers::VipsDriver.new - comparison = build_comparison_for(driver, "a.png", "b.png") - service = SnapDiff::AnnotationService.new(driver.find_difference_region(comparison)) + test "#annotate_and_save_images writes annotated and heatmap images" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + driver = SnapDiff::Drivers::VipsDriver.new + comparison = build_comparison_for(driver, "a.png", "b.png") + service = SnapDiff::AnnotationService.new(driver.find_difference_region(comparison)) - service.annotate_and_save_images + service.annotate_and_save_images - assert_same_images "a-and-b.heatmap.diff.png", service.heatmap_diff_path - end + assert_same_images "a-and-b.heatmap.diff.png", service.heatmap_diff_path + end - test "#clean_tmp_files removes annotated and heatmap images" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - driver = SnapDiff::Drivers::VipsDriver.new - comparison = build_comparison_for(driver, "a.png", "b.png") - service = SnapDiff::AnnotationService.new(driver.find_difference_region(comparison)) - service.annotate_and_save_images + test "#clean_tmp_files removes annotated and heatmap images" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + driver = SnapDiff::Drivers::VipsDriver.new + comparison = build_comparison_for(driver, "a.png", "b.png") + service = SnapDiff::AnnotationService.new(driver.find_difference_region(comparison)) + service.annotate_and_save_images - assert_predicate service.heatmap_diff_path, :exist? + assert_predicate service.heatmap_diff_path, :exist? - service.clean_tmp_files + service.clean_tmp_files - assert_not service.annotated_image_path.exist?, "diff should be cleaned" - assert_not service.annotated_base_image_path.exist?, "base diff should be cleaned" - assert_not service.heatmap_diff_path.exist?, "heatmap diff should be cleaned" - end + assert_not service.annotated_image_path.exist?, "diff should be cleaned" + assert_not service.annotated_base_image_path.exist?, "base diff should be cleaned" + assert_not service.heatmap_diff_path.exist?, "heatmap diff should be cleaned" + end - test "#save_annotation_for bakes in a visibly different image when a skip_area is set" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - driver = SnapDiff::Drivers::VipsDriver.new - new_image = driver.from_file(TEST_IMAGES_DIR.join("a.png")) - base_image = driver.from_file(TEST_IMAGES_DIR.join("b.png")) + test "#save_annotation_for bakes in a visibly different image when a skip_area is set" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + driver = SnapDiff::Drivers::VipsDriver.new + new_image = driver.from_file(TEST_IMAGES_DIR.join("a.png")) + base_image = driver.from_file(TEST_IMAGES_DIR.join("b.png")) - with_skip_area = SnapDiff::Comparison::Images.new(new_image, base_image, {skip_area: [Region.new(0, 0, 10, 10)]}, driver, - @_tmpdir / "with_skip_area.png", @_tmpdir / "with_skip_area_base.png") - without_skip_area = SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, - @_tmpdir / "without_skip_area.png", @_tmpdir / "without_skip_area_base.png") + with_skip_area = SnapDiff::Comparison::Images.new(new_image, base_image, {skip_area: [Region.new(0, 0, 10, 10)]}, driver, + @_tmpdir / "with_skip_area.png", @_tmpdir / "with_skip_area_base.png") + without_skip_area = SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, + @_tmpdir / "without_skip_area.png", @_tmpdir / "without_skip_area_base.png") - service_with = SnapDiff::AnnotationService.new(driver.find_difference_region(with_skip_area)) - service_without = SnapDiff::AnnotationService.new(driver.find_difference_region(without_skip_area)) - service_with.annotate_and_save_images - service_without.annotate_and_save_images + service_with = SnapDiff::AnnotationService.new(driver.find_difference_region(with_skip_area)) + service_without = SnapDiff::AnnotationService.new(driver.find_difference_region(without_skip_area)) + service_with.annotate_and_save_images + service_without.annotate_and_save_images - assert_not FileUtils.compare_file(service_with.annotated_base_image_path.to_s, service_without.annotated_base_image_path.to_s), - "annotated base image should differ once a skip_area rectangle is drawn onto it" - end + assert_not FileUtils.compare_file(service_with.annotated_base_image_path.to_s, service_without.annotated_base_image_path.to_s), + "annotated base image should differ once a skip_area rectangle is drawn onto it" + end - private + private - def build_comparison_for(driver, *images) - new_image = driver.from_file(TEST_IMAGES_DIR.join(images.first)) - base_image = driver.from_file(TEST_IMAGES_DIR.join(images.last)) + def build_comparison_for(driver, *images) + new_image = driver.from_file(TEST_IMAGES_DIR.join(images.first)) + base_image = driver.from_file(TEST_IMAGES_DIR.join(images.last)) - SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, @_tmpdir / images.first, @_tmpdir / images.last) - end + SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, @_tmpdir / images.first, @_tmpdir / images.last) end end diff --git a/test/unit/area_calculator_test.rb b/test/unit/area_calculator_test.rb index 659a5084..a223cf32 100644 --- a/test/unit/area_calculator_test.rb +++ b/test/unit/area_calculator_test.rb @@ -1,76 +1,70 @@ # frozen_string_literal: true require "test_helper" -require "capybara/screenshot/diff/area_calculator" +require "snap_diff/area_calculator" -module Capybara - module Screenshot - module Diff - class AreaCalculatorTest < ActiveSupport::TestCase - class CalculateSkipAreaTest < self - test "#calculate_skip_area returns empty array when no skip areas overlap with crop area" do - skip_area = [[0, 0, 100, 100], [200, 200, 100, 100]] - crop_area = [100, 100, 100, 100] - calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) +class AreaCalculatorTest < ActiveSupport::TestCase + class CalculateSkipAreaTest < self + test "#calculate_skip_area returns empty array when no skip areas overlap with crop area" do + skip_area = [[0, 0, 100, 100], [200, 200, 100, 100]] + crop_area = [100, 100, 100, 100] + calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) - result = calculator.calculate_skip_area + result = calculator.calculate_skip_area - assert_empty result - end + assert_empty result + end - test "#calculate_skip_area returns intersecting regions when skip areas overlap with crop area" do - skip_area = [Region.new(50, 50, 150, 150)] - crop_area = Region.new(0, 0, 200, 200) - calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) + test "#calculate_skip_area returns intersecting regions when skip areas overlap with crop area" do + skip_area = [Region.new(50, 50, 150, 150)] + crop_area = Region.new(0, 0, 200, 200) + calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) - result = calculator.calculate_skip_area + result = calculator.calculate_skip_area - assert_equal [Region.new(50, 50, 150, 150)], result - end - end + assert_equal [Region.new(50, 50, 150, 150)], result + end + end - class InitializationTest < self - test "#initialize handles Region objects for skip areas correctly" do - skip_area = [Region.new(0, 0, 100, 100)] - crop_area = Region.new(0, 0, 200, 200) + class InitializationTest < self + test "#initialize handles Region objects for skip areas correctly" do + skip_area = [Region.new(0, 0, 100, 100)] + crop_area = Region.new(0, 0, 200, 200) - calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) + calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) - assert_equal [Region.new(0, 0, 100, 100)], calculator.calculate_skip_area - end + assert_equal [Region.new(0, 0, 100, 100)], calculator.calculate_skip_area + end - test "#initialize converts array coordinates to Region objects" do - skip_area = [[0, 0, 100, 100]] - crop_area = [0, 0, 200, 200] + test "#initialize converts array coordinates to Region objects" do + skip_area = [[0, 0, 100, 100]] + crop_area = [0, 0, 200, 200] - calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) - result = calculator.calculate_skip_area + calculator = SnapDiff::AreaCalculator.new(crop_area, skip_area) + result = calculator.calculate_skip_area - assert_equal 1, result.size - assert_kind_of Region, result.first - assert_equal [0, 0, 100, 100], - [result.first.left, result.first.top, result.first.right, result.first.bottom] - end - end + assert_equal 1, result.size + assert_kind_of Region, result.first + assert_equal [0, 0, 100, 100], + [result.first.left, result.first.top, result.first.right, result.first.bottom] + end + end - class EdgeCaseTest < self - test "#calculate_skip_area returns empty array when skip_areas is empty" do - calculator = SnapDiff::AreaCalculator.new([0, 0, 100, 100], []) + class EdgeCaseTest < self + test "#calculate_skip_area returns empty array when skip_areas is empty" do + calculator = SnapDiff::AreaCalculator.new([0, 0, 100, 100], []) - result = calculator.calculate_skip_area + result = calculator.calculate_skip_area - assert_empty result - end + assert_empty result + end - test "#calculate_skip_area returns nil when skip_areas is not provided (nil)" do - calculator = SnapDiff::AreaCalculator.new([0, 0, 100, 100], nil) + test "#calculate_skip_area returns nil when skip_areas is not provided (nil)" do + calculator = SnapDiff::AreaCalculator.new([0, 0, 100, 100], nil) - result = calculator.calculate_skip_area + result = calculator.calculate_skip_area - assert_nil result - end - end - end + assert_nil result end end end diff --git a/test/unit/attempts_reporter_test.rb b/test/unit/attempts_reporter_test.rb index 1f8d02ff..7f512274 100644 --- a/test/unit/attempts_reporter_test.rb +++ b/test/unit/attempts_reporter_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" +require "snap_diff" module SnapDiff # Guard #2 from the v2 core-redesign acceptance contract (D8). @@ -13,7 +13,7 @@ module SnapDiff # data raised later at verify time. class AttemptsReporterTest < ActiveSupport::TestCase setup do - @manager = SnapDiff::SnapManager.new(Capybara::Screenshot.root / "attempts_reporter_test") + @manager = SnapDiff::SnapManager.new(SnapDiff.config.root / "attempts_reporter_test") @manager.create_output_directory_for end @@ -60,7 +60,7 @@ def take_screenshot(screenshot_path) error = nil SnapDiff.config.stub(:screenshoter, alternating_screenshoter) do - error = assert_raises(CapybaraScreenshotDiff::UnstableImage) do + error = assert_raises(SnapDiff::UnstableImage) do StableScreenshoter .new({stability_time_limit: 0.05, wait: 0.2}, {driver: :chunky_png}) .take_comparison_screenshot(snap) diff --git a/test/unit/backtrace_filter_test.rb b/test/unit/backtrace_filter_test.rb index b347c533..540bb560 100644 --- a/test/unit/backtrace_filter_test.rb +++ b/test/unit/backtrace_filter_test.rb @@ -1,62 +1,60 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff/error_with_filtered_backtrace" +require "snap_diff/error_with_filtered_backtrace" -module CapybaraScreenshotDiff - class BacktraceFilterTest < ActiveSupport::TestCase - test "#filtered removes lines originating from the given lib directory" do - filter = SnapDiff::BacktraceFilter.new("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/app/lib/") +class BacktraceFilterTest < ActiveSupport::TestCase + test "#filtered removes lines originating from the given lib directory" do + filter = SnapDiff::BacktraceFilter.new("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/app/lib/") - result = filter.filtered([ - "/app/lib/capybara_screenshot_diff/foo.rb:1:in 'bar'", - "/app/test/some_test.rb:5:in 'test_thing'" - ]) + result = filter.filtered([ + "/app/lib/capybara_screenshot_diff/foo.rb:1:in 'bar'", + "/app/test/some_test.rb:5:in 'test_thing'" + ]) - assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result - end + assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result + end - test "#filtered removes lines from activesupport, minitest, and railties gems" do - filter = SnapDiff::BacktraceFilter.new("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/app/lib/") + test "#filtered removes lines from activesupport, minitest, and railties gems" do + filter = SnapDiff::BacktraceFilter.new("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/app/lib/") - result = filter.filtered([ - "/gems/activesupport-7.0.0/lib/foo.rb:1:in 'bar'", - "/gems/minitest-5.0.0/lib/minitest.rb:2:in 'run'", - "/gems/railties-7.0.0/lib/baz.rb:3:in 'call'", - "/app/test/some_test.rb:5:in 'test_thing'" - ]) + result = filter.filtered([ + "/gems/activesupport-7.0.0/lib/foo.rb:1:in 'bar'", + "/gems/minitest-5.0.0/lib/minitest.rb:2:in 'run'", + "/gems/railties-7.0.0/lib/baz.rb:3:in 'call'", + "/app/test/some_test.rb:5:in 'test_thing'" + ]) - assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result - end + assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result + end - test "#filtered keeps lines outside the lib directory and unrelated gems" do - filter = SnapDiff::BacktraceFilter.new("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/app/lib/") - backtrace = [ - "/app/test/some_test.rb:5:in 'test_thing'", - "/gems/rack-3.0.0/lib/rack.rb:1:in 'call'" - ] + test "#filtered keeps lines outside the lib directory and unrelated gems" do + filter = SnapDiff::BacktraceFilter.new("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/app/lib/") + backtrace = [ + "/app/test/some_test.rb:5:in 'test_thing'", + "/gems/rack-3.0.0/lib/rack.rb:1:in 'call'" + ] - assert_equal backtrace, filter.filtered(backtrace) - end + assert_equal backtrace, filter.filtered(backtrace) + end - test "#filtered does not treat a sibling directory sharing the prefix as inside lib" do - filter = SnapDiff::BacktraceFilter.new("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/app/lib") + test "#filtered does not treat a sibling directory sharing the prefix as inside lib" do + filter = SnapDiff::BacktraceFilter.new("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/app/lib") - backtrace = ["/app/library/foo.rb:1:in 'bar'"] + backtrace = ["/app/library/foo.rb:1:in 'bar'"] - assert_equal backtrace, filter.filtered(backtrace) - end + assert_equal backtrace, filter.filtered(backtrace) + end - test "#initialize defaults to the library's own lib directory" do - filter = SnapDiff::BacktraceFilter.new - lib_file = File.expand_path("../../lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb", __dir__) + test "#initialize defaults to the library's own lib directory" do + filter = SnapDiff::BacktraceFilter.new + lib_file = File.expand_path("../../lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb", __dir__) - result = filter.filtered([ - "#{lib_file}:1:in 'filtered'", - "/app/test/some_test.rb:5:in 'test_thing'" - ]) + result = filter.filtered([ + "#{lib_file}:1:in 'filtered'", + "/app/test/some_test.rb:5:in 'test_thing'" + ]) - assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result - end + assert_equal ["/app/test/some_test.rb:5:in 'test_thing'"], result end end diff --git a/test/unit/capture/viewport_test.rb b/test/unit/capture/viewport_test.rb index bb536775..121ce251 100644 --- a/test/unit/capture/viewport_test.rb +++ b/test/unit/capture/viewport_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" +require "snap_diff" module SnapDiff module Capture @@ -17,7 +17,7 @@ class ViewportTest < ActiveSupport::TestCase test "prepare! raises WindowSizeMismatchError when the window size is wrong" do BrowserHelpers.stub(:window_size_is_wrong?, true) do BrowserHelpers.stub(:selenium?, false) do - error = assert_raises(CapybaraScreenshotDiff::WindowSizeMismatchError) do + error = assert_raises(SnapDiff::WindowSizeMismatchError) do Viewport.prepare!([800, 600]) end assert_includes error.message, "[800, 600]" diff --git a/test/unit/compare_api_test.rb b/test/unit/compare_api_test.rb index ba66adc9..811dbf97 100644 --- a/test/unit/compare_api_test.rb +++ b/test/unit/compare_api_test.rb @@ -2,59 +2,56 @@ require "test_helper" -module Capybara - module Screenshot - module Diff - class CompareApiTest < ActiveSupport::TestCase - test ".compare returns ImageCompare instance" do - result = Diff.compare( - TEST_IMAGES_DIR / "a.png", - TEST_IMAGES_DIR / "a.png" - ) - assert_kind_of SnapDiff::Comparison, result - end - - test ".compare detects identical images" do - result = Diff.compare( - TEST_IMAGES_DIR / "a.png", - TEST_IMAGES_DIR / "a.png" - ) - assert result.quick_equal? - assert_not result.different? - end - - test ".compare detects different images" do - result = Diff.compare( - TEST_IMAGES_DIR / "a.png", - TEST_IMAGES_DIR / "b.png" - ) - assert_not result.quick_equal? - assert result.different? - end - - test ".compare accepts driver option" do - skip "VIPS not present" unless defined?(Vips) - - result = Diff.compare( - TEST_IMAGES_DIR / "a.png", - TEST_IMAGES_DIR / "a.png", - driver: :vips - ) - assert result.quick_equal? - end - - test ".compare accepts tolerance options" do - skip "VIPS not present" unless defined?(Vips) - - result = Diff.compare( - TEST_IMAGES_DIR / "a.png", - TEST_IMAGES_DIR / "b.png", - driver: :vips, - tolerance: 1.0 - ) - assert_not result.different? - end - end - end +# The canonical entry point for the file-to-file compare API. The v1 +# `Capybara::Screenshot::Diff.compare` forwarder over it is exercised in +# test/legacy/legacy_forwarders_test.rb. +class CompareApiTest < ActiveSupport::TestCase + test ".compare returns ImageCompare instance" do + result = SnapDiff.compare( + TEST_IMAGES_DIR / "a.png", + TEST_IMAGES_DIR / "a.png" + ) + assert_kind_of SnapDiff::Comparison, result + end + + test ".compare detects identical images" do + result = SnapDiff.compare( + TEST_IMAGES_DIR / "a.png", + TEST_IMAGES_DIR / "a.png" + ) + assert result.quick_equal? + assert_not result.different? + end + + test ".compare detects different images" do + result = SnapDiff.compare( + TEST_IMAGES_DIR / "a.png", + TEST_IMAGES_DIR / "b.png" + ) + assert_not result.quick_equal? + assert result.different? + end + + test ".compare accepts driver option" do + skip "VIPS not present" unless defined?(Vips) + + result = SnapDiff.compare( + TEST_IMAGES_DIR / "a.png", + TEST_IMAGES_DIR / "a.png", + driver: :vips + ) + assert result.quick_equal? + end + + test ".compare accepts tolerance options" do + skip "VIPS not present" unless defined?(Vips) + + result = SnapDiff.compare( + TEST_IMAGES_DIR / "a.png", + TEST_IMAGES_DIR / "b.png", + driver: :vips, + tolerance: 1.0 + ) + assert_not result.different? end end diff --git a/test/unit/config_default_timing_test.rb b/test/unit/config_default_timing_test.rb index 53cd535c..59e8977e 100644 --- a/test/unit/config_default_timing_test.rb +++ b/test/unit/config_default_timing_test.rb @@ -107,7 +107,7 @@ def check_both(name, expected, mod, mattr) # 3) Capybara-coupled wait is read at CALL time (live), not frozen. Capybara.default_max_wait_time = 42.5 check("default_options[:wait] follows Capybara.default_max_wait_time set after require", - 42.5, Capybara::Screenshot::Diff.default_options[:wait]) + 42.5, SnapDiff.config.default_options[:wait]) RUBY # Probe B: ENV["CI"] present (non-empty) BEFORE the require flips the diff --git a/test/unit/core_tree_has_no_legacy_deps_test.rb b/test/unit/core_tree_has_no_legacy_deps_test.rb index e68e6b34..04e89527 100644 --- a/test/unit/core_tree_has_no_legacy_deps_test.rb +++ b/test/unit/core_tree_has_no_legacy_deps_test.rb @@ -17,7 +17,7 @@ # because the generator for the v1 surface has to be code, and the v1 trees # have to stay alias-only -- they are legacy by design and go with it. # -# WHOLE-LINE comments are ignored: "ex +Capybara::Screenshot.active?+" on its +# WHOLE-LINE comments are ignored: "ex +SnapDiff.config.active?+" on its # own line is history, not a dependency. Everything else on a code line # counts, strings and trailing comments included -- a user-facing message # naming a legacy accessor is a legacy reference that survives the deletion diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb index 1c3c0344..6f282ce1 100644 --- a/test/unit/diff_test.rb +++ b/test/unit/diff_test.rb @@ -4,249 +4,245 @@ require "minitest/stub_const" require "support/non_minitest_assertions" -module Capybara - module Screenshot - class DiffTest < ActiveSupport::TestCase - setup do - Capybara.current_driver = Capybara.default_driver +class DiffTest < ActiveSupport::TestCase + setup do + Capybara.current_driver = Capybara.default_driver - @orig_add_driver_path = Capybara::Screenshot.add_driver_path - Capybara::Screenshot.add_driver_path = true + @orig_add_driver_path = SnapDiff.config.add_driver_path + SnapDiff.config.add_driver_path = true - @orig_add_os_path = Capybara::Screenshot.add_os_path - Capybara::Screenshot.add_os_path = true + @orig_add_os_path = SnapDiff.config.add_os_path + SnapDiff.config.add_os_path = true - @orig_screenshot_format = Capybara::Screenshot.screenshot_format + @orig_screenshot_format = SnapDiff.config.screenshot_format - @orig_window_size = Capybara::Screenshot.window_size - Capybara::Screenshot.window_size = [80, 80] - end + @orig_window_size = SnapDiff.config.window_size + SnapDiff.config.window_size = [80, 80] + end - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::Minitest::Assertions - include CapybaraScreenshotDiff::DSLStub + include Capybara::Screenshot::Diff + include SnapDiff::Minitest::Assertions + include DSLStub - teardown do - SnapDiff::SnapManager.cleanup! unless persist_comparisons? - CapybaraScreenshotDiff.reset + teardown do + SnapDiff::SnapManager.cleanup! unless persist_comparisons? + SnapDiff.reset - Capybara::Screenshot.add_driver_path = @orig_add_driver_path - Capybara::Screenshot.add_os_path = @orig_add_os_path - Capybara::Screenshot.screenshot_format = @orig_screenshot_format - Capybara::Screenshot.window_size = @orig_window_size - end + SnapDiff.config.add_driver_path = @orig_add_driver_path + SnapDiff.config.add_os_path = @orig_add_os_path + SnapDiff.config.screenshot_format = @orig_screenshot_format + SnapDiff.config.window_size = @orig_window_size + end - test "has a version number" do - refute_nil ::Capybara::Screenshot::Diff::VERSION - end + test "has a version number" do + refute_nil ::Capybara::Screenshot::Diff::VERSION + end - test "updates screenshot group name" do - assert_nil screenshot_namer.group - screenshot_group "a" - assert_equal "a", screenshot_namer.group - screenshot_group "b" - assert_equal "b", screenshot_namer.group - end + test "updates screenshot group name" do + assert_nil screenshot_namer.group + screenshot_group "a" + assert_equal "a", screenshot_namer.group + screenshot_group "b" + assert_equal "b", screenshot_namer.group + end - test "screenshot_section prepends section to path" do - assert_nil screenshot_namer.section - assert_nil screenshot_namer.group + test "screenshot_section prepends section to path" do + assert_nil screenshot_namer.section + assert_nil screenshot_namer.group - screenshot_section "a" - assert_equal "a", screenshot_namer.section - assert_match %r{doc/screenshots/(macos|linux)/rack_test/a}, screenshot_dir + screenshot_section "a" + assert_equal "a", screenshot_namer.section + assert_match %r{doc/screenshots/(macos|linux)/rack_test/a}, screenshot_dir - screenshot_group "b" - assert_equal "b", screenshot_namer.group - assert_match %r{doc/screenshots/(macos|linux)/rack_test/a/b}, screenshot_dir + screenshot_group "b" + assert_equal "b", screenshot_namer.group + assert_match %r{doc/screenshots/(macos|linux)/rack_test/a/b}, screenshot_dir - screenshot_group "c" - assert_equal "c", screenshot_namer.group - assert_match %r{doc/screenshots/(macos|linux)/rack_test/a/c}, screenshot_dir - end + screenshot_group "c" + assert_equal "c", screenshot_namer.group + assert_match %r{doc/screenshots/(macos|linux)/rack_test/a/c}, screenshot_dir + end - test "stores screenshot with given name" do - screenshot_group "screenshot" - assert_matches_screenshot "a" - end + test "stores screenshot with given name" do + screenshot_group "screenshot" + assert_matches_screenshot "a" + end - test "does not fail when fail_on_difference is false and screenshots differ" do - SnapDiff.config.stub(:fail_on_difference, false) do - test_case = SampleMiniTestCase.new(:_test_sample_screenshot_error) - test_case.run - assert_equal 0, test_case.failures.size - end - end + test "does not fail when fail_on_difference is false and screenshots differ" do + SnapDiff.config.stub(:fail_on_difference, false) do + test_case = SampleMiniTestCase.new(:_test_sample_screenshot_error) + test_case.run + assert_equal 0, test_case.failures.size + end + end - test "writes screenshot to alternate save path" do - default_path = Capybara::Screenshot.save_path - Capybara::Screenshot.save_path = "foo/bar" + test "writes screenshot to alternate save path" do + default_path = SnapDiff.config.save_path + SnapDiff.config.save_path = "foo/bar" - screenshot_section "a" - screenshot_group "b" - screenshot "a", delayed: false + screenshot_section "a" + screenshot_group "b" + screenshot "a", delayed: false - assert_match %r{foo/bar/(macos|linux)/rack_test/a/b}, screenshot_dir - ensure - FileUtils.remove_entry Capybara::Screenshot.screenshot_area_abs - Capybara::Screenshot.save_path = default_path - end + assert_match %r{foo/bar/(macos|linux)/rack_test/a/b}, screenshot_dir + ensure + FileUtils.remove_entry SnapDiff.config.screenshot_area_abs + SnapDiff.config.save_path = default_path + end - test "does not error when using stability_time_limit" do - default_stability_time_limit = Capybara::Screenshot.stability_time_limit - Capybara::Screenshot.stability_time_limit = 0.001 + test "does not error when using stability_time_limit" do + default_stability_time_limit = SnapDiff.config.stability_time_limit + SnapDiff.config.stability_time_limit = 0.001 - screenshot "a" - ensure - Capybara::Screenshot.stability_time_limit = default_stability_time_limit - end + screenshot "a" + ensure + SnapDiff.config.stability_time_limit = default_stability_time_limit + end - test "builds full name from string" do - assert_equal "a", build_full_name("a") - screenshot_group "b" - assert_equal "b/00_a", build_full_name("a") - screenshot_section "c" - assert_equal "c/b/00_a", build_full_name("a") - screenshot_group nil - assert_equal "c/a", build_full_name("a") - end + test "builds full name from string" do + assert_equal "a", build_full_name("a") + screenshot_group "b" + assert_equal "b/00_a", build_full_name("a") + screenshot_section "c" + assert_equal "c/b/00_a", build_full_name("a") + screenshot_group nil + assert_equal "c/a", build_full_name("a") + end - test "builds full name from symbol" do - screenshot_group :b - assert_equal "b/00_a", build_full_name(:a) - end + test "builds full name from symbol" do + screenshot_group :b + assert_equal "b/00_a", build_full_name(:a) + end - test "detects available diff drivers" do - # NOTE for tests we are loading both drivers, so we expect that all of them are available - expected_drivers = defined?(Vips) ? %i[vips chunky_png] : %i[chunky_png] + test "detects available diff drivers" do + # NOTE for tests we are loading both drivers, so we expect that all of them are available + expected_drivers = defined?(Vips) ? %i[vips chunky_png] : %i[chunky_png] - assert_equal expected_drivers, Capybara::Screenshot::Diff::AVAILABLE_DRIVERS - end + assert_equal expected_drivers, SnapDiff::Drivers::AVAILABLE_DRIVERS + end - test "aggregates failures on teardown for Minitest" do - test_case = SampleMiniTestCase.new(:_test_sample_screenshot_error) + test "aggregates failures on teardown for Minitest" do + test_case = SampleMiniTestCase.new(:_test_sample_screenshot_error) - test_case.run + test_case.run - assert_equal 1, test_case.failures.size - assert_includes test_case.failures.first.message, "expected error message" - end + assert_equal 1, test_case.failures.size + assert_includes test_case.failures.first.message, "expected error message" + end - test "raises error on teardown for non-Minitest" do - test_case = SampleNotMiniTestCase.new - test_case._test_sample_screenshot_error + test "raises error on teardown for non-Minitest" do + test_case = SampleNotMiniTestCase.new + test_case._test_sample_screenshot_error - expected_message = - "Screenshot does not match for 'sample_screenshot' expected error message for non minitest" - assert_raises(CapybaraScreenshotDiff::ExpectationNotMet, expected_message) { test_case.teardown } - assert_empty(CapybaraScreenshotDiff.assertions) - end + expected_message = + "Screenshot does not match for 'sample_screenshot' expected error message for non minitest" + assert_raises(SnapDiff::ExpectationNotMet, expected_message) { test_case.teardown } + assert_empty(SnapDiff.session.assertions) + end - class SampleMiniTestCase < ActiveSupport::TestCase - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::Minitest::Assertions - - # NOTE: we need to add `_` as prefix to skip this test from auto-run - def _test_sample_screenshot_error - mock = ::Minitest::Mock.new - mock.expect(:different?, true) - mock.expect(:different?, true) - mock.expect(:dimensions_changed?, false) - mock.expect(:base_image_path, Pathname.new("screenshot.base.png")) - mock.expect(:error_message, "expected error message") - - assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") - assertion.caller = ["my_test.rb:42"] - assertion.compare = mock - CapybaraScreenshotDiff.add_assertion(assertion) - - assert true - end - end + class SampleMiniTestCase < ActiveSupport::TestCase + include Capybara::Screenshot::Diff + include SnapDiff::Minitest::Assertions + + # NOTE: we need to add `_` as prefix to skip this test from auto-run + def _test_sample_screenshot_error + mock = ::Minitest::Mock.new + mock.expect(:different?, true) + mock.expect(:different?, true) + mock.expect(:dimensions_changed?, false) + mock.expect(:base_image_path, Pathname.new("screenshot.base.png")) + mock.expect(:error_message, "expected error message") + + assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") + assertion.caller = ["my_test.rb:42"] + assertion.compare = mock + SnapDiff.session.add_assertion(assertion) + + assert true + end + end - class SampleNotMiniTestCase - def self.setup - # noop - end - - def self.teardown(&block) - @@teardown_callback = block - end - - def teardown - instance_eval(&@@teardown_callback) if @@teardown_callback - ensure - @@teardown_callback = nil - CapybaraScreenshotDiff.reset - end - - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::NonMinitest::Assertions - - def _test_sample_screenshot_error - comparison = ::Minitest::Mock.new - comparison.expect(:different?, true) # to find backtrace - comparison.expect(:different?, true) # to find messages - comparison.expect(:dimensions_changed?, false) - comparison.expect(:base_image_path, Pathname.new("screenshot.base.png")) - comparison.expect(:error_message, "expected error message for non minitest") - - assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") - assertion.caller = ["my_test.rb:42"] - assertion.compare = comparison - CapybaraScreenshotDiff.add_assertion(assertion) - end - end + class SampleNotMiniTestCase + def self.setup + # noop + end - class ScreenshotFormatTest < ActiveSupport::TestCase - setup do - @orig_screenshot_format = Capybara::Screenshot.screenshot_format - end + def self.teardown(&block) + @@teardown_callback = block + end - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::DSLStub - include CapybaraScreenshotDiff::Minitest::Assertions + def teardown + instance_eval(&@@teardown_callback) if @@teardown_callback + ensure + @@teardown_callback = nil + SnapDiff.reset + end - teardown do - Capybara::Screenshot.screenshot_format = @orig_screenshot_format - end + include Capybara::Screenshot::Diff + include NonMinitest::Assertions + + def _test_sample_screenshot_error + comparison = ::Minitest::Mock.new + comparison.expect(:different?, true) # to find backtrace + comparison.expect(:different?, true) # to find messages + comparison.expect(:dimensions_changed?, false) + comparison.expect(:base_image_path, Pathname.new("screenshot.base.png")) + comparison.expect(:error_message, "expected error message for non minitest") + + assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") + assertion.caller = ["my_test.rb:42"] + assertion.compare = comparison + SnapDiff.session.add_assertion(assertion) + end + end - test "stores screenshot using default format extension" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - snap = SnapDiff::SnapManager.snapshot("a", "webp") + class ScreenshotFormatTest < ActiveSupport::TestCase + setup do + @orig_screenshot_format = SnapDiff.config.screenshot_format + end - set_test_images(snap, :a, :a) + include Capybara::Screenshot::Diff + include DSLStub + include SnapDiff::Minitest::Assertions - SnapDiff.config.stub(:screenshot_format, "webp") do - screenshot "a", driver: :vips + teardown do + SnapDiff.config.screenshot_format = @orig_screenshot_format + end - assert_stored_screenshot("a.webp") - end - end + test "stores screenshot using default format extension" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + snap = SnapDiff::SnapManager.snapshot("a", "webp") - test "stores screenshot using overridden format extension" do - snap = SnapDiff::SnapManager.snapshot("a", "png") - set_test_images(snap, :a, :a) + set_test_images(snap, :a, :a) - SnapDiff.config.stub(:screenshot_format, "webp") do - screenshot "a", screenshot_format: "png" + SnapDiff.config.stub(:screenshot_format, "webp") do + screenshot "a", driver: :vips - assert_stored_screenshot("a.png") - end - end + assert_stored_screenshot("a.webp") end + end - def screenshot_dir - File.join(Capybara::Screenshot.screenshot_area, *screenshot_namer.directory_parts) - end + test "stores screenshot using overridden format extension" do + snap = SnapDiff::SnapManager.snapshot("a", "png") + set_test_images(snap, :a, :a) - def screenshot_namer - CapybaraScreenshotDiff.screenshot_namer - end + SnapDiff.config.stub(:screenshot_format, "webp") do + screenshot "a", screenshot_format: "png" - def build_full_name(name) - CapybaraScreenshotDiff.screenshot_namer.full_name(name) + assert_stored_screenshot("a.png") end end end + + def screenshot_dir + File.join(SnapDiff.config.screenshot_area, *screenshot_namer.directory_parts) + end + + def screenshot_namer + SnapDiff.session.screenshot_namer + end + + def build_full_name(name) + SnapDiff.session.screenshot_namer.full_name(name) + end end diff --git a/test/unit/difference_test.rb b/test/unit/difference_test.rb index bc254986..3acde90f 100644 --- a/test/unit/difference_test.rb +++ b/test/unit/difference_test.rb @@ -1,30 +1,28 @@ # frozen_string_literal: true require "test_helper" -require "capybara/screenshot/diff/difference" +require "snap_diff/comparison_result" -module Capybara::Screenshot::Diff - class DifferenceTest < ActiveSupport::TestCase - setup do - @difference = SnapDiff::ComparisonResult.new(nil, {}, nil, {different_dimensions: []}) - end +class DifferenceTest < ActiveSupport::TestCase + setup do + @difference = SnapDiff::ComparisonResult.new(nil, {}, nil, {different_dimensions: []}) + end - test "#different? returns true when images have different dimensions" do - assert_predicate @difference, :different? - end + test "#different? returns true when images have different dimensions" do + assert_predicate @difference, :different? + end - test "#failed? returns true when images have different dimensions" do - assert_predicate @difference, :failed? - end + test "#failed? returns true when images have different dimensions" do + assert_predicate @difference, :failed? + end - test "#inspect is a one-line summary with the difference metrics" do - line = @difference.inspect + test "#inspect is a one-line summary with the difference metrics" do + line = @difference.inspect - assert_includes line, "different=true" - assert_includes line, "failed_by=" - assert_includes line, "area_size=0" - assert_includes line, "difference_level=" - assert_not_includes line, "\n" - end + assert_includes line, "different=true" + assert_includes line, "failed_by=" + assert_includes line, "area_size=0" + assert_includes line, "difference_level=" + assert_not_includes line, "\n" end end diff --git a/test/unit/driver_coverage_test.rb b/test/unit/driver_coverage_test.rb index 0e55c0d1..3f4d9605 100644 --- a/test/unit/driver_coverage_test.rb +++ b/test/unit/driver_coverage_test.rb @@ -3,40 +3,38 @@ require "test_helper" require "support/driver_coverage" -module CapybaraScreenshotDiff - class DriverCoverageTest < ActiveSupport::TestCase - test "#banner lists detected drivers" do - assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png, vips", - DriverCoverage.banner(%i[chunky_png vips]) - end - - test "#banner calls out unavailable drivers" do - assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png | unavailable: vips", - DriverCoverage.banner(%i[chunky_png]) - end - - test "#missing_for_ci returns nothing when CI is unset" do - assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: nil) - end - - test "#missing_for_ci returns nothing when CI is blank" do - assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "") - end - - test "#missing_for_ci returns nothing in CI when both drivers are available" do - assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "true") - end - - test "#missing_for_ci flags vips missing in CI" do - assert_equal [:vips], DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true") - end - - test "#missing_for_ci flags chunky_png missing in CI" do - assert_equal [:chunky_png], DriverCoverage.missing_for_ci(%i[vips], ci: "true") - end - - test "#missing_for_ci honors an explicit exclude override" do - assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true", exclude: [:vips]) - end +class DriverCoverageTest < ActiveSupport::TestCase + test "#banner lists detected drivers" do + assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png, vips", + DriverCoverage.banner(%i[chunky_png vips]) + end + + test "#banner calls out unavailable drivers" do + assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png | unavailable: vips", + DriverCoverage.banner(%i[chunky_png]) + end + + test "#missing_for_ci returns nothing when CI is unset" do + assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: nil) + end + + test "#missing_for_ci returns nothing when CI is blank" do + assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "") + end + + test "#missing_for_ci returns nothing in CI when both drivers are available" do + assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "true") + end + + test "#missing_for_ci flags vips missing in CI" do + assert_equal [:vips], DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true") + end + + test "#missing_for_ci flags chunky_png missing in CI" do + assert_equal [:chunky_png], DriverCoverage.missing_for_ci(%i[vips], ci: "true") + end + + test "#missing_for_ci honors an explicit exclude override" do + assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true", exclude: [:vips]) end end diff --git a/test/unit/drivers/chunky_png_driver_test.rb b/test/unit/drivers/chunky_png_driver_test.rb index 232135dc..809606f7 100644 --- a/test/unit/drivers/chunky_png_driver_test.rb +++ b/test/unit/drivers/chunky_png_driver_test.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true require "test_helper" -require "capybara/screenshot/diff/image_compare" -require "capybara/screenshot/diff/drivers/chunky_png_driver" +require "snap_diff/comparison" +require "snap_diff/drivers/chunky_png_driver" require "support/driver_contract_tests" # Nested in the canonical SnapDiff::Drivers namespace: reopening the old -# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers +# SnapDiff::Drivers path would define a fresh Drivers # module there, shadowing the v2 step 6 lazy const_missing forwarder. module SnapDiff module Drivers class ChunkyPNGDriverTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub + include DSLStub include DriverContractTests class QuickEqualTest < self diff --git a/test/unit/drivers/utils_test.rb b/test/unit/drivers/utils_test.rb index a0abbb48..c2054253 100644 --- a/test/unit/drivers/utils_test.rb +++ b/test/unit/drivers/utils_test.rb @@ -1,57 +1,51 @@ # frozen_string_literal: true require "test_helper" -require "capybara/screenshot/diff/utils" +require "snap_diff/utils" require "minitest/stub_const" -module Capybara - module Screenshot - module Diff - class UtilsTest < ActiveSupport::TestCase - test "#detect_available_drivers includes :vips when ruby-vips gem is available" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - Object.stub :require, ->(gem) { gem == "vips" } do - assert_includes SnapDiff::Utils.detect_available_drivers, :vips - end - end +class UtilsTest < ActiveSupport::TestCase + test "#detect_available_drivers includes :vips when ruby-vips gem is available" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + Object.stub :require, ->(gem) { gem == "vips" } do + assert_includes SnapDiff::Utils.detect_available_drivers, :vips + end + end - test "#detect_available_drivers excludes :vips when ruby-vips gem is not available" do - Object.stub_remove_const(:Vips) do - Object.stub :require, ->(gem) { gem != "vips" } do - assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips - end - end - end + test "#detect_available_drivers excludes :vips when ruby-vips gem is not available" do + Object.stub_remove_const(:Vips) do + Object.stub :require, ->(gem) { gem != "vips" } do + assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips + end + end + end - test "#detect_available_drivers excludes :vips when system libvips is not installed" do - Object.stub_remove_const(:Vips) do - Object.stub :require, ->(gem) { gem == "vips" && raise(LoadError.new("Could not ... vips")) } do - assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips - end - end - end + test "#detect_available_drivers excludes :vips when system libvips is not installed" do + Object.stub_remove_const(:Vips) do + Object.stub :require, ->(gem) { gem == "vips" && raise(LoadError.new("Could not ... vips")) } do + assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips + end + end + end - test "#detect_available_drivers returns drivers in order of preference when multiple are available" do - Object.stub_consts(Vips: Class.new, ChunkyPNG: Class.new) do - Object.stub :require, true do - assert_equal %i[vips chunky_png], SnapDiff::Utils.detect_available_drivers - end - end - end + test "#detect_available_drivers returns drivers in order of preference when multiple are available" do + Object.stub_consts(Vips: Class.new, ChunkyPNG: Class.new) do + Object.stub :require, true do + assert_equal %i[vips chunky_png], SnapDiff::Utils.detect_available_drivers + end + end + end - test "#detect_available_drivers includes :chunky_png when the gem is available" do - Object.stub :require, ->(gem) { gem == "chunky_png" } do - assert_includes SnapDiff::Utils.detect_available_drivers, :chunky_png - end - end + test "#detect_available_drivers includes :chunky_png when the gem is available" do + Object.stub :require, ->(gem) { gem == "chunky_png" } do + assert_includes SnapDiff::Utils.detect_available_drivers, :chunky_png + end + end - test "#detect_available_drivers excludes :chunky_png when the gem is not available" do - Object.stub_remove_const(:ChunkyPNG) do - Object.stub :require, ->(gem) { gem != "chunky_png" } do - assert_not_includes SnapDiff::Utils.detect_available_drivers, :chunky_png - end - end - end + test "#detect_available_drivers excludes :chunky_png when the gem is not available" do + Object.stub_remove_const(:ChunkyPNG) do + Object.stub :require, ->(gem) { gem != "chunky_png" } do + assert_not_includes SnapDiff::Utils.detect_available_drivers, :chunky_png end end end diff --git a/test/unit/drivers/vips_driver_test.rb b/test/unit/drivers/vips_driver_test.rb index f4dd279c..f10e86fa 100644 --- a/test/unit/drivers/vips_driver_test.rb +++ b/test/unit/drivers/vips_driver_test.rb @@ -3,15 +3,15 @@ require "test_helper" require "support/driver_contract_tests" -require "capybara/screenshot/diff/drivers/vips_driver" if defined?(Vips) +require "snap_diff/drivers/vips_driver" if defined?(Vips) # Nested in the canonical SnapDiff::Drivers namespace: reopening the old -# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers +# SnapDiff::Drivers path would define a fresh Drivers # module there, shadowing the v2 step 6 lazy const_missing forwarder. module SnapDiff module Drivers class VipsDriverTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub + include DSLStub include DriverContractTests setup do diff --git a/test/unit/drivers_test.rb b/test/unit/drivers_test.rb index c7531f92..bd4eaa1d 100644 --- a/test/unit/drivers_test.rb +++ b/test/unit/drivers_test.rb @@ -6,7 +6,7 @@ # SnapDiff::Drivers is the canonical driver registry (docs/snapdiff.md). # Until 3.0 readiness work, `.available` read the value out of -# Capybara::Screenshot::Diff::AVAILABLE_DRIVERS, which is defined by +# SnapDiff::Drivers::AVAILABLE_DRIVERS, which is defined by # config_legacy.rb -- so a documented canonical API only worked when the v1 # tree happened to be loaded. class DriversTest < ActiveSupport::TestCase diff --git a/test/unit/dsl_test.rb b/test/unit/dsl_test.rb index cdbcf37a..ee3158e7 100644 --- a/test/unit/dsl_test.rb +++ b/test/unit/dsl_test.rb @@ -1,261 +1,259 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" -require "capybara_screenshot_diff/screenshot_assertion" - -module CapybaraScreenshotDiff - class DSLTest < ActiveSupport::TestCase - include SnapDiff::DSL - include CapybaraScreenshotDiff::DSLStub - - def before_setup - @original_root = Capybara::Screenshot.root - @new_root = Dir.mktmpdir - Capybara::Screenshot.root = Pathname.new(@new_root) - super - end +require "snap_diff" +require "snap_diff/screenshot_assertion" + +class DSLTest < ActiveSupport::TestCase + include SnapDiff::DSL + include DSLStub + + def before_setup + @original_root = SnapDiff.config.root + @new_root = Dir.mktmpdir + SnapDiff.config.root = Pathname.new(@new_root) + super + end - def after_teardown - super - Capybara::Screenshot.root = @original_root - FileUtils.remove_entry(@new_root) if @new_root - end + def after_teardown + super + SnapDiff.config.root = @original_root + FileUtils.remove_entry(@new_root) if @new_root + end - test "#screenshot raises error when screenshot is missing and fail_if_new is true" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - SnapDiff.config.stub(:fail_if_new, true) do - assert_raises CapybaraScreenshotDiff::ExpectationNotMet, match: /No existing screenshot found for/ do - screenshot "not_existing_screenshot-name" - end + test "#screenshot raises error when screenshot is missing and fail_if_new is true" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + SnapDiff.config.stub(:fail_if_new, true) do + assert_raises SnapDiff::ExpectationNotMet, match: /No existing screenshot found for/ do + screenshot "not_existing_screenshot-name" end end end + end - test "#assert_image_not_changed generates correct error message for image mismatch" do - message = assert_image_not_changed(["my_test.rb:42"], "name", make_comparison(:a, :c, destination: "screenshot.png")) - value = (RUBY_VERSION >= "2.4") ? 187.4 : 188 - assert_equal <<~MSG.chomp, message - Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value}}) - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.base.diff.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.diff.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.heatmap.diff.png - my_test.rb:42 - MSG - end + test "#assert_image_not_changed generates correct error message for image mismatch" do + message = assert_image_not_changed(["my_test.rb:42"], "name", make_comparison(:a, :c, destination: "screenshot.png")) + value = (RUBY_VERSION >= "2.4") ? 187.4 : 188 + assert_equal <<~MSG.chomp, message + Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value}}) + #{SnapDiff.config.root}/doc/screenshots/screenshot.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.base.diff.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.diff.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.heatmap.diff.png + my_test.rb:42 + MSG + end - test "#assert_image_not_changed includes shift distance in error message when specified" do - message = assert_image_not_changed( - ["my_test.rb:42"], - "name", - make_comparison(:a, :c, destination: "screenshot.png", shift_distance_limit: 1, driver: :chunky_png) - ) - value = (RUBY_VERSION >= "2.4") ? 5.0 : 5 - assert_equal <<~MSG.chomp, message - Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value},"max_shift_distance":15}) - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.base.diff.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.diff.png - #{Capybara::Screenshot.root}/doc/screenshots/screenshot.heatmap.diff.png - my_test.rb:42 - MSG - end + test "#assert_image_not_changed includes shift distance in error message when specified" do + message = assert_image_not_changed( + ["my_test.rb:42"], + "name", + make_comparison(:a, :c, destination: "screenshot.png", shift_distance_limit: 1, driver: :chunky_png) + ) + value = (RUBY_VERSION >= "2.4") ? 5.0 : 5 + assert_equal <<~MSG.chomp, message + Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value},"max_shift_distance":15}) + #{SnapDiff.config.root}/doc/screenshots/screenshot.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.base.diff.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.diff.png + #{SnapDiff.config.root}/doc/screenshots/screenshot.heatmap.diff.png + my_test.rb:42 + MSG + end - test "#screenshot supports driver options for image comparison" do - skip "vips is disabled" unless defined?(Vips) - assert_not screenshot("a", driver: :vips) - end + test "#screenshot supports driver options for image comparison" do + skip "vips is disabled" unless defined?(Vips) + assert_not screenshot("a", driver: :vips) + end - def assert_no_screenshot_jobs_scheduled - assert_not_predicate CapybaraScreenshotDiff.registry, :assertions_present? - end + def assert_no_screenshot_jobs_scheduled + assert_not_predicate SnapDiff.session, :assertions_present? + end - test "#screenshot with skip_stack_frames: 0 includes our_screenshot in caller" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - assert_no_screenshot_jobs_scheduled + test "#screenshot with skip_stack_frames: 0 includes our_screenshot in caller" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + assert_no_screenshot_jobs_scheduled - snap = create_snapshot_for(:a, :c) + snap = create_snapshot_for(:a, :c) - our_screenshot(snap.full_name, 0) - assert_equal 1, CapybaraScreenshotDiff.assertions.size - assert_match(/our_screenshot'/, CapybaraScreenshotDiff.assertions[0].caller.first) - assert_equal snap.full_name, CapybaraScreenshotDiff.assertions[0].name - end + our_screenshot(snap.full_name, 0) + assert_equal 1, SnapDiff.session.assertions.size + assert_match(/our_screenshot'/, SnapDiff.session.assertions[0].caller.first) + assert_equal snap.full_name, SnapDiff.session.assertions[0].name end + end - test "#screenshot with skip_stack_frames: 1 includes test method in caller" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - assert_no_screenshot_jobs_scheduled + test "#screenshot with skip_stack_frames: 1 includes test method in caller" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + assert_no_screenshot_jobs_scheduled - snap = create_snapshot_for(:a, :c) + snap = create_snapshot_for(:a, :c) - our_screenshot(snap.full_name, 1) - assert_equal 1, CapybaraScreenshotDiff.assertions.size - assert_match( - %r{/dsl_test.rb}, - CapybaraScreenshotDiff.assertions[0].caller.first - ) - assert_equal snap.full_name, CapybaraScreenshotDiff.assertions[0].name - end + our_screenshot(snap.full_name, 1) + assert_equal 1, SnapDiff.session.assertions.size + assert_match( + %r{/dsl_test.rb}, + SnapDiff.session.assertions[0].caller.first + ) + assert_equal snap.full_name, SnapDiff.session.assertions[0].name end + end - test "#assert_no_screenshot_changes reports caller from test method" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - assert_no_screenshot_jobs_scheduled + test "#assert_no_screenshot_changes reports caller from test method" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + assert_no_screenshot_jobs_scheduled - snap = create_snapshot_for(:a, :c) + snap = create_snapshot_for(:a, :c) - assert_no_screenshot_changes(snap.full_name) - assert_equal 1, CapybaraScreenshotDiff.assertions.size - assert_match( - %r{/dsl_test.rb}, - CapybaraScreenshotDiff.assertions[0].caller.first - ) - end + assert_no_screenshot_changes(snap.full_name) + assert_equal 1, SnapDiff.session.assertions.size + assert_match( + %r{/dsl_test.rb}, + SnapDiff.session.assertions[0].caller.first + ) end + end - test "#screenshot with delayed: false raises error when images differ" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff.config.stub(:delayed, false) do - assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) do - snap = create_snapshot_for(:c, :a) - screenshot(snap.full_name, delayed: false) - end + test "#screenshot with delayed: false raises error when images differ" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff.config.stub(:delayed, false) do + assert_raises(SnapDiff::ExpectationNotMet) do + snap = create_snapshot_for(:c, :a) + screenshot(snap.full_name, delayed: false) end end end + end - test "#screenshot with delayed: false succeeds when images match" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff.config.stub(:delayed, false) do - snap = create_snapshot_for(:a) - assert_nothing_raised { screenshot(snap.full_name, delayed: false) } - end + test "#screenshot with delayed: false succeeds when images match" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff.config.stub(:delayed, false) do + snap = create_snapshot_for(:a) + assert_nothing_raised { screenshot(snap.full_name, delayed: false) } end end + end - test "#screenshot accepts skip_area and stability_time_limit options" do - assert_not screenshot(:a, skip_area: [0, 0, 1, 1], stability_time_limit: 0.01) - end + test "#screenshot accepts skip_area and stability_time_limit options" do + assert_not screenshot(:a, skip_area: [0, 0, 1, 1], stability_time_limit: 0.01) + end - test "#screenshot creates new screenshot file when it doesn't exist" do - screenshot(:c) + test "#screenshot creates new screenshot file when it doesn't exist" do + screenshot(:c) - snap = SnapDiff::SnapManager.snapshot("c") - assert_predicate snap.path, :exist? - end + snap = SnapDiff::SnapManager.snapshot("c") + assert_predicate snap.path, :exist? + end - # Regression for https://github.com/snap-diff/snap_diff-capybara/issues/191: - # a user-defined #screenshot in the test class must not hijack the gem's internals. - test "#assert_no_screenshot_changes ignores user-defined #screenshot" do - def self.screenshot(*, **) - @user_screenshot_called = true - end + # Regression for https://github.com/snap-diff/snap_diff-capybara/issues/191: + # a user-defined #screenshot in the test class must not hijack the gem's internals. + test "#assert_no_screenshot_changes ignores user-defined #screenshot" do + def self.screenshot(*, **) + @user_screenshot_called = true + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :c) + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :c) - assert_no_screenshot_changes(snap.full_name) - assert_not @user_screenshot_called - assert_equal 1, CapybaraScreenshotDiff.assertions.size - end + assert_no_screenshot_changes(snap.full_name) + assert_not @user_screenshot_called + assert_equal 1, SnapDiff.session.assertions.size end + end - test "#assert_matches_screenshot ignores user-defined #screenshot" do - def self.screenshot(*, **) - @user_screenshot_called = true - end + test "#assert_matches_screenshot ignores user-defined #screenshot" do + def self.screenshot(*, **) + @user_screenshot_called = true + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :c) + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :c) - assert_matches_screenshot(snap.full_name) - assert_not @user_screenshot_called - assert_equal 1, CapybaraScreenshotDiff.assertions.size - end + assert_matches_screenshot(snap.full_name) + assert_not @user_screenshot_called + assert_equal 1, SnapDiff.session.assertions.size end + end - test "#screenshot records new screenshots that have no baseline in the registry" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - screenshot "a" + test "#screenshot records new screenshots that have no baseline in the registry" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + screenshot "a" - assert_equal ["a"], CapybaraScreenshotDiff.new_screenshots - end + assert_equal ["a"], SnapDiff.session.new_screenshots end + end - test "CapybaraScreenshotDiff.reset clears new_screenshots" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - screenshot "a" - assert_predicate CapybaraScreenshotDiff, :new_screenshots_present? + test "SnapDiff.reset clears new_screenshots" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + screenshot "a" + assert_predicate CapybaraScreenshotDiff, :new_screenshots_present? - CapybaraScreenshotDiff.reset + SnapDiff.reset - assert_not_predicate CapybaraScreenshotDiff, :new_screenshots_present? - assert_empty CapybaraScreenshotDiff.new_screenshots - end + assert_not_predicate CapybaraScreenshotDiff, :new_screenshots_present? + assert_empty SnapDiff.session.new_screenshots end + end - test "#capture_screenshot writes the file and registers no assertion" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - capture_screenshot(:c) + test "#capture_screenshot writes the file and registers no assertion" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + capture_screenshot(:c) - snap = SnapDiff::SnapManager.snapshot("c") - assert_predicate snap.path, :exist? - assert_no_screenshot_jobs_scheduled - end + snap = SnapDiff::SnapManager.snapshot("c") + assert_predicate snap.path, :exist? + assert_no_screenshot_jobs_scheduled end + end - test "#capture_screenshot creates the destination directory for nested names" do - naive_screenshoter = Class.new do - def initialize(_capture_options, _comparison_options) - end + test "#capture_screenshot creates the destination directory for nested names" do + naive_screenshoter = Class.new do + def initialize(_capture_options, _comparison_options) + end - def take_comparison_screenshot(snapshot) - File.binwrite(snapshot.path, "png") - end + def take_comparison_screenshot(snapshot) + File.binwrite(snapshot.path, "png") end + end - SnapDiff.config.stub(:screenshoter, naive_screenshoter) do - capture_screenshot("nested/dir/example") + SnapDiff.config.stub(:screenshoter, naive_screenshoter) do + capture_screenshot("nested/dir/example") - assert_predicate SnapDiff::SnapManager.snapshot("nested/dir/example").path, :exist? - end + assert_predicate SnapDiff::SnapManager.snapshot("nested/dir/example").path, :exist? end + end - test "#capture_screenshot does not raise even when a differing baseline exists" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :c) + test "#capture_screenshot does not raise even when a differing baseline exists" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :c) - assert_nothing_raised { capture_screenshot(snap.full_name) } - assert_no_screenshot_jobs_scheduled - end + assert_nothing_raised { capture_screenshot(snap.full_name) } + assert_no_screenshot_jobs_scheduled end + end - test "#screenshot with compare: false captures without registering an assertion" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :c) - snap.path.delete + test "#screenshot with compare: false captures without registering an assertion" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :c) + snap.path.delete - screenshot(snap.full_name, compare: false) + screenshot(snap.full_name, compare: false) - assert_predicate snap.path, :exist? - assert_no_screenshot_jobs_scheduled - end + assert_predicate snap.path, :exist? + assert_no_screenshot_jobs_scheduled end + end - private + private - def our_screenshot(name, skip_stack_frames) - screenshot(name, skip_stack_frames: skip_stack_frames) - end + def our_screenshot(name, skip_stack_frames) + screenshot(name, skip_stack_frames: skip_stack_frames) + end - # Pins the user-facing error-message shape produced by #validate. - def assert_image_not_changed(backtrace, name, comparison) - assertion = SnapDiff::ScreenshotAssertion.new(name) - assertion.caller = backtrace - assertion.compare = comparison - assertion.validate - end + # Pins the user-facing error-message shape produced by #validate. + def assert_image_not_changed(backtrace, name, comparison) + assertion = SnapDiff::ScreenshotAssertion.new(name) + assertion.caller = backtrace + assertion.compare = comparison + assertion.validate end end diff --git a/test/unit/image_compare_test.rb b/test/unit/image_compare_test.rb index cd4be258..ae631046 100644 --- a/test/unit/image_compare_test.rb +++ b/test/unit/image_compare_test.rb @@ -2,232 +2,226 @@ require "test_helper" require "minitest/stub_const" -require "capybara/screenshot/diff/drivers/chunky_png_driver" +require "snap_diff/drivers/chunky_png_driver" if defined?(Vips) - require "capybara/screenshot/diff/drivers/vips_driver" + require "snap_diff/drivers/vips_driver" elsif ENV["SCREENSHOT_DRIVER"] == "vips" raise 'Required `ruby-vips` gem or `vips` library is missing. Ensure "ruby-vips" gem and "vips" library is installed.' end -module Capybara - module Screenshot - module Diff - class ImageCompareTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub +class ImageCompareTest < ActiveSupport::TestCase + include DSLStub - test "#initialize creates instance with chunky_png driver by default" do - comparison = make_comparison(:b) - assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver - end - - test "#initialize creates instance with explicit chunky_png driver" do - comparison = make_comparison(:b, driver: :chunky_png) - assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver - end + test "#initialize creates instance with chunky_png driver by default" do + comparison = make_comparison(:b) + assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver + end - test "#initialize creates instance with vips driver when specified" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - comparison = make_comparison(:b, driver: :vips) - assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver - end + test "#initialize creates instance with explicit chunky_png driver" do + comparison = make_comparison(:b, driver: :chunky_png) + assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver + end - test "#different? with vips driver generates annotated diff images" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - comparison = make_comparison(:a, :b, driver: :vips) + test "#initialize creates instance with vips driver when specified" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + comparison = make_comparison(:b, driver: :vips) + assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver + end - assert comparison.different? + test "#different? with vips driver generates annotated diff images" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + comparison = make_comparison(:a, :b, driver: :vips) - assert_same_images("a-and-b.diff.png", comparison.reporter.annotated_base_image_path) - assert_same_images("b-and-a.diff.png", comparison.reporter.annotated_image_path) - end + assert comparison.different? - test "#different? handles very long input filenames with vips driver" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - filename = %w[this-0000000000000000000000000000000000000000000000000-path/is/extremely/ - long/and/if/the/directories/are/flattened/in/ - the_temporary_they_will_cause_the_filename_to_exceed_ - the_limit_on_most_unix_systems_which_nobody_wants.png].join - comparison = make_comparison(:a, :b, destination: (Rails.root / filename), driver: :vips) + assert_same_images("a-and-b.diff.png", comparison.reporter.annotated_base_image_path) + assert_same_images("b-and-a.diff.png", comparison.reporter.annotated_image_path) + end - assert comparison.different? - end + test "#different? handles very long input filenames with vips driver" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + filename = %w[this-0000000000000000000000000000000000000000000000000-path/is/extremely/ + long/and/if/the/directories/are/flattened/in/ + the_temporary_they_will_cause_the_filename_to_exceed_ + the_limit_on_most_unix_systems_which_nobody_wants.png].join + comparison = make_comparison(:a, :b, destination: (Rails.root / filename), driver: :vips) - test "#initialize with vips driver respects tolerance option" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - comp = make_comparison(:a, :b, driver: :vips, tolerance: 0.02) - assert comp.quick_equal? - assert_not comp.different? - end + assert comparison.different? + end - test "#initialize with chunky_png driver respects tolerance option" do - comp = make_comparison(:a, :b, driver: :chunky_png, tolerance: 0.02) - assert comp.quick_equal? - assert_not comp.different? - assert_equal 0.02, comp.driver_options[:tolerance] - end + test "#initialize with vips driver respects tolerance option" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + comp = make_comparison(:a, :b, driver: :vips, tolerance: 0.02) + assert comp.quick_equal? + assert_not comp.different? + end - test "#initialize with dimensions creates valid comparison" do - comp = make_comparison(:b, dimensions: [80, 80]) - assert comp.quick_equal? - assert_not comp.different? - end + test "#initialize with chunky_png driver respects tolerance option" do + comp = make_comparison(:a, :b, driver: :chunky_png, tolerance: 0.02) + assert comp.quick_equal? + assert_not comp.different? + assert_equal 0.02, comp.driver_options[:tolerance] + end - test "#initialize with :auto driver selects vips when available" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - comparison = make_comparison(:b, driver: :auto) - assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver - end + test "#initialize with dimensions creates valid comparison" do + comp = make_comparison(:b, dimensions: [80, 80]) + assert comp.quick_equal? + assert_not comp.different? + end - test "#initialize with :auto driver raises error when no drivers available" do - # Canonical stubbing point since the detected-drivers list moved to - # SnapDiff::Drivers (3.0 readiness). The legacy - # Capybara::Screenshot::Diff::AVAILABLE_DRIVERS is now an eager - # same-object ALIAS of this constant, so stubbing the old name only - # rebinds the alias and no longer reaches the core. - SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) do - assert_raise(RuntimeError) do - comparison = make_comparison(:b, driver: :auto) - assert comparison.quick_equal? - end - end - end - end + test "#initialize with :auto driver selects vips when available" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + comparison = make_comparison(:b, driver: :auto) + assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver + end - # Guards the regression killed twice during ADR-004 review (migration-plan PR 5): skip_area - # masking must run at *comparison* time, against whatever base image is on disk (including a - # baseline checked out from VCS), not baked in at *capture* time. Capture-time masking would - # only ever touch the freshly-taken screenshot; a VCS-checked-out baseline predates that - # capture and would never get masked, so a masked new image compared against an unmasked - # baseline would still report a difference in the skip area. - # - # `make_comparison(:a, :c)` stands in for that scenario: `:a` plays the already-on-disk - # baseline (as if checked out from VCS), `:c` plays the freshly captured screenshot. The two - # fixtures are known to differ only within [11,3,48,20] (see ChunkyPNGDriverTest above). - class SkipAreaMasksVcsBaselineTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - - test "#different? masks the VCS-checked-out baseline, not just the new screenshot" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - - full_image_region = Region.from_edge_coordinates(0, 0, 80, 80) - comparison = make_comparison(:a, :c, destination: "skip_area_vcs_baseline", driver: :vips, skip_area: [full_image_region]) - - refute_predicate comparison, :different? - end + test "#initialize with :auto driver raises error when no drivers available" do + # Canonical stubbing point since the detected-drivers list moved to + # SnapDiff::Drivers (3.0 readiness). The legacy + # SnapDiff::Drivers::AVAILABLE_DRIVERS is now an eager + # same-object ALIAS of this constant, so stubbing the old name only + # rebinds the alias and no longer reaches the core. + SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) do + assert_raise(RuntimeError) do + comparison = make_comparison(:b, driver: :auto) + assert comparison.quick_equal? end + end + end +end - class IntegrationRegressionTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - - AVAILABLE_DRIVERS = [{}, {driver: :chunky_png}] - - test "identical images are quick_equal and not different across all drivers" do - images = all_fixtures_images_names - AVAILABLE_DRIVERS.each do |driver| - Dir.chdir File.expand_path("../fixtures/images", __dir__) do - images.each do |old_img| - new_img = old_img - comparison = make_comparison(old_img, new_img, **driver) - assert( - comparison.quick_equal?, - "compare #{old_img} with #{new_img} with #{driver} driver should be quick_equal" - ) - assert_not( - comparison.different?, - "compare #{old_img} with #{new_img} with #{driver} driver should not be different" - ) - end - end - end - end +# Guards the regression killed twice during ADR-004 review (migration-plan PR 5): skip_area +# masking must run at *comparison* time, against whatever base image is on disk (including a +# baseline checked out from VCS), not baked in at *capture* time. Capture-time masking would +# only ever touch the freshly-taken screenshot; a VCS-checked-out baseline predates that +# capture and would never get masked, so a masked new image compared against an unmasked +# baseline would still report a difference in the skip area. +# +# `make_comparison(:a, :c)` stands in for that scenario: `:a` plays the already-on-disk +# baseline (as if checked out from VCS), `:c` plays the freshly captured screenshot. The two +# fixtures are known to differ only within [11,3,48,20] (see ChunkyPNGDriverTest above). +class SkipAreaMasksVcsBaselineTest < ActiveSupport::TestCase + include DSLStub + + test "#different? masks the VCS-checked-out baseline, not just the new screenshot" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + + full_image_region = Region.from_edge_coordinates(0, 0, 80, 80) + comparison = make_comparison(:a, :c, destination: "skip_area_vcs_baseline", driver: :vips, skip_area: [full_image_region]) + + refute_predicate comparison, :different? + end +end - test "different images are not quick_equal and are marked as different" do - images = all_fixtures_images_names - - AVAILABLE_DRIVERS.each do |driver| - images.each do |image| - other_images = images - [image] - other_images.each do |different_image| - comparison = make_comparison(image, different_image, **driver) - assert_not( - comparison.quick_equal?, - "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should not be quick_equal" - ) - assert( - comparison.different?, - "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should be different" - ) - end - end - end +class IntegrationRegressionTest < ActiveSupport::TestCase + include DSLStub + + AVAILABLE_DRIVERS = [{}, {driver: :chunky_png}] + + test "identical images are quick_equal and not different across all drivers" do + images = all_fixtures_images_names + AVAILABLE_DRIVERS.each do |driver| + Dir.chdir File.expand_path("../fixtures/images", __dir__) do + images.each do |old_img| + new_img = old_img + comparison = make_comparison(old_img, new_img, **driver) + assert( + comparison.quick_equal?, + "compare #{old_img} with #{new_img} with #{driver} driver should be quick_equal" + ) + assert_not( + comparison.different?, + "compare #{old_img} with #{new_img} with #{driver} driver should not be different" + ) end + end + end + end - def all_fixtures_images_names - %w[a a_cropped b c d portrait portrait_b] + test "different images are not quick_equal and are marked as different" do + images = all_fixtures_images_names + + AVAILABLE_DRIVERS.each do |driver| + images.each do |image| + other_images = images - [image] + other_images.each do |different_image| + comparison = make_comparison(image, different_image, **driver) + assert_not( + comparison.quick_equal?, + "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should not be quick_equal" + ) + assert( + comparison.different?, + "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should be different" + ) end end + end + end - class ImageCompareRefactorTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - include TestHelpers + def all_fixtures_images_names + %w[a a_cropped b c d portrait portrait_b] + end +end - # Test #quick_equal? method - test "#quick_equal? returns true when comparing identical images" do - comparison = make_comparison(:a, :a) - assert_predicate comparison, :quick_equal? - end +class ImageCompareRefactorTest < ActiveSupport::TestCase + include DSLStub + include TestHelpers - test "#quick_equal? returns false when comparing different images" do - comparison = make_comparison(:a, :b) - refute_predicate comparison, :quick_equal? - end + # Test #quick_equal? method + test "#quick_equal? returns true when comparing identical images" do + comparison = make_comparison(:a, :a) + assert_predicate comparison, :quick_equal? + end - test "#quick_equal? skips the expensive region scan when pixels differ and no tolerance options are set" do - comparison = make_comparison(:a, :b) - region_scan_calls = 0 - comparison.driver.define_singleton_method(:find_difference_region) do |*args| - region_scan_calls += 1 - Capybara::Screenshot::Diff::TestDoubles::TestDifference.new(true) - end + test "#quick_equal? returns false when comparing different images" do + comparison = make_comparison(:a, :b) + refute_predicate comparison, :quick_equal? + end - comparison.quick_equal? + test "#quick_equal? skips the expensive region scan when pixels differ and no tolerance options are set" do + comparison = make_comparison(:a, :b) + region_scan_calls = 0 + comparison.driver.define_singleton_method(:find_difference_region) do |*args| + region_scan_calls += 1 + TestDoubles::TestDifference.new(true) + end - assert_equal 0, region_scan_calls, "find_difference_region should not run when no tolerance options are configured" - end + comparison.quick_equal? - # Test #different? method - test "#different? returns false when comparing identical images" do - comparison = make_comparison(:a, :a) - refute_predicate comparison, :different? - end + assert_equal 0, region_scan_calls, "find_difference_region should not run when no tolerance options are configured" + end - test "#different? returns true when comparing different images" do - comparison = make_comparison(:a, :b) - assert_predicate comparison, :different? - end + # Test #different? method + test "#different? returns false when comparing identical images" do + comparison = make_comparison(:a, :a) + refute_predicate comparison, :different? + end - # Test #dimensions_changed? method - test "#dimensions_changed? returns true when images have different dimensions" do - comparison = make_comparison(:portrait, :a) - comparison.processed + test "#different? returns true when comparing different images" do + comparison = make_comparison(:a, :b) + assert_predicate comparison, :different? + end - assert_predicate comparison, :dimensions_changed? - assert_kind_of SnapDiff::Reporters::Default, comparison.reporter - end + # Test #dimensions_changed? method + test "#dimensions_changed? returns true when images have different dimensions" do + comparison = make_comparison(:portrait, :a) + comparison.processed - test "#dimensions_changed? returns false when images have same dimensions" do - comparison = make_comparison(:a, :a) - comparison.processed + assert_predicate comparison, :dimensions_changed? + assert_kind_of SnapDiff::Reporters::Default, comparison.reporter + end - refute_predicate comparison, :dimensions_changed? - end + test "#dimensions_changed? returns false when images have same dimensions" do + comparison = make_comparison(:a, :a) + comparison.processed - # Test reporter configuration - test "#reporter returns Default reporter by default" do - comparison = make_comparison(:a, :a) - assert_kind_of SnapDiff::Reporters::Default, comparison.reporter - end - end - end + refute_predicate comparison, :dimensions_changed? + end + + # Test reporter configuration + test "#reporter returns Default reporter by default" do + comparison = make_comparison(:a, :a) + assert_kind_of SnapDiff::Reporters::Default, comparison.reporter end end diff --git a/test/unit/image_preprocessor_test.rb b/test/unit/image_preprocessor_test.rb index 99350e37..21e67d0a 100644 --- a/test/unit/image_preprocessor_test.rb +++ b/test/unit/image_preprocessor_test.rb @@ -4,93 +4,87 @@ require "support/test_doubles" require "support/test_helpers" -module Capybara - module Screenshot - module Diff - class ImagePreprocessorTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - include TestHelpers +class ImagePreprocessorTest < ActiveSupport::TestCase + include DSLStub + include TestHelpers - def setup - super - @driver = create_test_driver - end - - test "#process_comparison returns comparison unchanged when no preprocessing options are provided" do - preprocessor = SnapDiff::ImagePreprocessor.new(@driver, {}) - comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) + def setup + super + @driver = create_test_driver + end - result = preprocessor.process_comparison(comparison) + test "#process_comparison returns comparison unchanged when no preprocessing options are provided" do + preprocessor = SnapDiff::ImagePreprocessor.new(@driver, {}) + comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) - assert_equal comparison, result - assert_empty @driver.add_black_box_calls - assert_empty @driver.filter_calls - end + result = preprocessor.process_comparison(comparison) - test "#process_comparison applies black box to skip areas when skip_area option is provided" do - skip_area = [{x: 10, y: 20, width: 30, height: 40}] - preprocessor = SnapDiff::ImagePreprocessor.new(@driver, skip_area: skip_area) - comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) + assert_equal comparison, result + assert_empty @driver.add_black_box_calls + assert_empty @driver.filter_calls + end - result = preprocessor.process_comparison(comparison) + test "#process_comparison applies black box to skip areas when skip_area option is provided" do + skip_area = [{x: 10, y: 20, width: 30, height: 40}] + preprocessor = SnapDiff::ImagePreprocessor.new(@driver, skip_area: skip_area) + comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) - assert_equal comparison, result - assert_equal 2, @driver.add_black_box_calls.size + result = preprocessor.process_comparison(comparison) - first_call = @driver.add_black_box_calls[0] - second_call = @driver.add_black_box_calls[1] + assert_equal comparison, result + assert_equal 2, @driver.add_black_box_calls.size - assert_equal skip_area.first, first_call[:region] - assert_equal skip_area.first, second_call[:region] - assert_equal :base_image, first_call[:image] - assert_equal :new_image, second_call[:image] - end + first_call = @driver.add_black_box_calls[0] + second_call = @driver.add_black_box_calls[1] - test "#process_comparison applies median filter when VipsDriver is available and median_filter_window_size is specified" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + assert_equal skip_area.first, first_call[:region] + assert_equal skip_area.first, second_call[:region] + assert_equal :base_image, first_call[:image] + assert_equal :new_image, second_call[:image] + end - @driver = create_test_driver(is_vips: true) - window_size = 3 - options = {median_filter_window_size: window_size} - preprocessor = SnapDiff::ImagePreprocessor.new(@driver, options) - comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) + test "#process_comparison applies median filter when VipsDriver is available and median_filter_window_size is specified" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - result = preprocessor.process_comparison(comparison) + @driver = create_test_driver(is_vips: true) + window_size = 3 + options = {median_filter_window_size: window_size} + preprocessor = SnapDiff::ImagePreprocessor.new(@driver, options) + comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) - assert_equal comparison, result - assert_equal 2, @driver.filter_calls.size + result = preprocessor.process_comparison(comparison) - first_call = @driver.filter_calls[0] - second_call = @driver.filter_calls[1] + assert_equal comparison, result + assert_equal 2, @driver.filter_calls.size - assert_equal window_size, first_call[:size] - assert_equal window_size, second_call[:size] - assert_equal :base_image, first_call[:image] - assert_equal :new_image, second_call[:image] - end + first_call = @driver.filter_calls[0] + second_call = @driver.filter_calls[1] - test "process_comparison warns and skips median filter when VipsDriver is not available" do - window_size = 3 - options = { - median_filter_window_size: window_size, - image_path: "some/path.png" - } + assert_equal window_size, first_call[:size] + assert_equal window_size, second_call[:size] + assert_equal :base_image, first_call[:image] + assert_equal :new_image, second_call[:image] + end - expected_warning = /Median filter has been skipped for.*because it is not supported/ + test "process_comparison warns and skips median filter when VipsDriver is not available" do + window_size = 3 + options = { + median_filter_window_size: window_size, + image_path: "some/path.png" + } - comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) + expected_warning = /Median filter has been skipped for.*because it is not supported/ - warning_output = capture_io do - preprocessor = SnapDiff::ImagePreprocessor.new(@driver, options) - result = preprocessor.process_comparison(comparison) + comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver) - assert_equal comparison, result - assert_empty @driver.filter_calls - end + warning_output = capture_io do + preprocessor = SnapDiff::ImagePreprocessor.new(@driver, options) + result = preprocessor.process_comparison(comparison) - assert_match expected_warning, warning_output.join - end - end + assert_equal comparison, result + assert_empty @driver.filter_calls end + + assert_match expected_warning, warning_output.join end end diff --git a/test/unit/minitest_assertions_test.rb b/test/unit/minitest_assertions_test.rb index 7f4a62af..64963d4d 100644 --- a/test/unit/minitest_assertions_test.rb +++ b/test/unit/minitest_assertions_test.rb @@ -2,73 +2,71 @@ require "test_helper" -module CapybaraScreenshotDiff - class MinitestAssertionsTest < ActiveSupport::TestCase - # Runs a throwaway ::Minitest::Test that takes a single screenshot, so we can - # inspect how before_teardown resolved (passed/skipped/failed) without polluting - # the outer test's own assertions/reporting. - # - # @param teardown [Proc, nil] optional replacement `teardown` method, to - # simulate a user teardown that runs after `before_teardown`. Calls - # `super()` first so DSLStub's own cleanup still happens. - def run_inner_test(teardown: nil, &block) - test_class = Class.new(::Minitest::Test) do - include CapybaraScreenshotDiff::Minitest::Assertions - include CapybaraScreenshotDiff::DSLStub +class MinitestAssertionsTest < ActiveSupport::TestCase + # Runs a throwaway ::Minitest::Test that takes a single screenshot, so we can + # inspect how before_teardown resolved (passed/skipped/failed) without polluting + # the outer test's own assertions/reporting. + # + # @param teardown [Proc, nil] optional replacement `teardown` method, to + # simulate a user teardown that runs after `before_teardown`. Calls + # `super()` first so DSLStub's own cleanup still happens. + def run_inner_test(teardown: nil, &block) + test_class = Class.new(::Minitest::Test) do + include SnapDiff::Minitest::Assertions + include DSLStub - define_method(:test_it, &block) - define_method(:teardown, &teardown) if teardown - end - test_class.new(:test_it).run + define_method(:test_it, &block) + define_method(:teardown, &teardown) if teardown end + test_class.new(:test_it).run + end - test "#before_teardown skips the test when pending_if_new is enabled and a screenshot has no baseline" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - SnapDiff.config.stub(:pending_if_new, true) do - result = run_inner_test { screenshot("a") } + test "#before_teardown skips the test when pending_if_new is enabled and a screenshot has no baseline" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + SnapDiff.config.stub(:pending_if_new, true) do + result = run_inner_test { screenshot("a") } - assert_predicate result, :skipped? - assert_equal( - "No baseline for: a. Commit the captured screenshots to record them.", - result.failures.first.message - ) - end + assert_predicate result, :skipped? + assert_equal( + "No baseline for: a. Commit the captured screenshots to record them.", + result.failures.first.message + ) end end + end - test "#screenshot and #assert_no_screenshot_changes count Minitest assertions" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - result = run_inner_test do - screenshot("a") - assert_no_screenshot_changes("b") - end - - assert_predicate result, :passed? - assert_equal 2, result.assertions + test "#screenshot and #assert_no_screenshot_changes count Minitest assertions" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + result = run_inner_test do + screenshot("a") + assert_no_screenshot_changes("b") end + + assert_predicate result, :passed? + assert_equal 2, result.assertions end + end - test "#before_teardown does not mask a real teardown error behind a pending skip" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - SnapDiff.config.stub(:pending_if_new, true) do - result = run_inner_test(teardown: proc { - super() - raise "boom from teardown" - }) { screenshot("a") } + test "#before_teardown does not mask a real teardown error behind a pending skip" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + SnapDiff.config.stub(:pending_if_new, true) do + result = run_inner_test(teardown: proc { + super() + raise "boom from teardown" + }) { screenshot("a") } - refute_predicate result, :skipped? - assert_predicate result, :error? - end + refute_predicate result, :skipped? + assert_predicate result, :error? end end + end - test "#before_teardown does not skip the test when pending_if_new is disabled" do - SnapDiff::Vcs.stub(:checkout_vcs, false) do - SnapDiff.config.stub(:pending_if_new, false) do - result = run_inner_test { screenshot("a") } + test "#before_teardown does not skip the test when pending_if_new is disabled" do + SnapDiff::Vcs.stub(:checkout_vcs, false) do + SnapDiff.config.stub(:pending_if_new, false) do + result = run_inner_test { screenshot("a") } - assert_predicate result, :passed? - end + assert_predicate result, :passed? end end end diff --git a/test/unit/pending_screenshots_message_test.rb b/test/unit/pending_screenshots_message_test.rb index 90d4175f..f4db3fbc 100644 --- a/test/unit/pending_screenshots_message_test.rb +++ b/test/unit/pending_screenshots_message_test.rb @@ -2,51 +2,49 @@ require "test_helper" -module CapybaraScreenshotDiff - class PendingScreenshotsMessageTest < ActiveSupport::TestCase - teardown do - CapybaraScreenshotDiff.reset - end +class PendingScreenshotsMessageTest < ActiveSupport::TestCase + teardown do + SnapDiff.reset + end - test "returns nil when pending_if_new is disabled" do - SnapDiff.config.stub(:pending_if_new, false) do - CapybaraScreenshotDiff.record_new_screenshot("a") + test "returns nil when pending_if_new is disabled" do + SnapDiff.config.stub(:pending_if_new, false) do + SnapDiff.session.record_new_screenshot("a") - assert_nil CapybaraScreenshotDiff.pending_screenshots_message - end + assert_nil SnapDiff.pending_screenshots_message end + end - test "returns nil when pending_if_new is enabled but no new screenshots were recorded" do - SnapDiff.config.stub(:pending_if_new, true) do - assert_nil CapybaraScreenshotDiff.pending_screenshots_message - end + test "returns nil when pending_if_new is enabled but no new screenshots were recorded" do + SnapDiff.config.stub(:pending_if_new, true) do + assert_nil SnapDiff.pending_screenshots_message end + end - test "returns the baseline message listing recorded screenshot names" do - SnapDiff.config.stub(:pending_if_new, true) do - CapybaraScreenshotDiff.record_new_screenshot("a") - CapybaraScreenshotDiff.record_new_screenshot("b") + test "returns the baseline message listing recorded screenshot names" do + SnapDiff.config.stub(:pending_if_new, true) do + SnapDiff.session.record_new_screenshot("a") + SnapDiff.session.record_new_screenshot("b") - assert_equal( - "No baseline for: a, b. Commit the captured screenshots to record them.", - CapybaraScreenshotDiff.pending_screenshots_message - ) - end + assert_equal( + "No baseline for: a, b. Commit the captured screenshots to record them.", + SnapDiff.pending_screenshots_message + ) end + end - test "reads from the calling thread's own registry, not other threads'" do - SnapDiff.config.stub(:pending_if_new, true) do - other_thread_result = Thread.new { - CapybaraScreenshotDiff.record_new_screenshot("other-thread") - CapybaraScreenshotDiff.pending_screenshots_message - }.value - - assert_equal( - "No baseline for: other-thread. Commit the captured screenshots to record them.", - other_thread_result - ) - assert_nil CapybaraScreenshotDiff.pending_screenshots_message - end + test "reads from the calling thread's own registry, not other threads'" do + SnapDiff.config.stub(:pending_if_new, true) do + other_thread_result = Thread.new { + SnapDiff.session.record_new_screenshot("other-thread") + SnapDiff.pending_screenshots_message + }.value + + assert_equal( + "No baseline for: other-thread. Commit the captured screenshots to record them.", + other_thread_result + ) + assert_nil SnapDiff.pending_screenshots_message end end end diff --git a/test/unit/region_test.rb b/test/unit/region_test.rb index 9f2068c9..79935163 100644 --- a/test/unit/region_test.rb +++ b/test/unit/region_test.rb @@ -2,62 +2,60 @@ require "test_helper" -module Capybara::Screenshot::Diff - class RegionTest < ActiveSupport::TestCase - test "#move_by updates region coordinates by specified deltas" do - region = Region.new(10, 10, 10, 10).move_by(-5, -5) - - assert_equal 5, region.x - assert_equal 5, region.y - assert_equal 10, region.width - assert_equal 10, region.height - end - - test "#find_intersect_with returns intersection with another region" do - crop = Region.new(5, 5, 10, 10) - region = Region.new(10, 10, 20, 20).find_intersect_with(crop) - - assert_equal 10, region.x - assert_equal 10, region.y - assert_equal 5, region.width - assert_equal 5, region.height - end - - test "#find_relative_intersect returns intersection with relative coordinates" do - crop = Region.new(5, 5, 10, 10) - - region = crop.find_relative_intersect(Region.new(0, 0, 20, 20)) - - assert_equal 0, region.x - assert_equal 0, region.y - assert_equal 10, region.width - assert_equal 10, region.height - - region = crop.find_relative_intersect(Region.new(10, 10, 20, 20)) - - assert_equal 5, region.x - assert_equal 5, region.y - assert_equal 5, region.width - assert_equal 5, region.height - end - - test ".from_edge_coordinates returns nil when right or bottom is nil" do - assert_nil Region.from_edge_coordinates(0, 0, nil, nil) - end - - test ".from_edge_coordinates returns nil when region has zero or negative dimensions" do - assert_nil Region.from_edge_coordinates(10, 10, 9, 11) - assert_nil Region.from_edge_coordinates(10, 10, 11, 9) - end - - test "#== returns true when comparing with an identical Region" do - assert_equal Region.new(10, 10, 10, 10), Region.new(10, 10, 10, 10) - assert_not_equal Region.new(10, 10, 10, 10), Region.new(10, 10, 10, 11) - end - - test "#== returns true when comparing with equivalent Array of coordinates" do - assert_equal Region.new(10, 10, 10, 10), [10, 10, 10, 10] - assert_not_equal Region.new(10, 10, 10, 10), [10, 10, 10, 11] - end +class RegionTest < ActiveSupport::TestCase + test "#move_by updates region coordinates by specified deltas" do + region = Region.new(10, 10, 10, 10).move_by(-5, -5) + + assert_equal 5, region.x + assert_equal 5, region.y + assert_equal 10, region.width + assert_equal 10, region.height + end + + test "#find_intersect_with returns intersection with another region" do + crop = Region.new(5, 5, 10, 10) + region = Region.new(10, 10, 20, 20).find_intersect_with(crop) + + assert_equal 10, region.x + assert_equal 10, region.y + assert_equal 5, region.width + assert_equal 5, region.height + end + + test "#find_relative_intersect returns intersection with relative coordinates" do + crop = Region.new(5, 5, 10, 10) + + region = crop.find_relative_intersect(Region.new(0, 0, 20, 20)) + + assert_equal 0, region.x + assert_equal 0, region.y + assert_equal 10, region.width + assert_equal 10, region.height + + region = crop.find_relative_intersect(Region.new(10, 10, 20, 20)) + + assert_equal 5, region.x + assert_equal 5, region.y + assert_equal 5, region.width + assert_equal 5, region.height + end + + test ".from_edge_coordinates returns nil when right or bottom is nil" do + assert_nil Region.from_edge_coordinates(0, 0, nil, nil) + end + + test ".from_edge_coordinates returns nil when region has zero or negative dimensions" do + assert_nil Region.from_edge_coordinates(10, 10, 9, 11) + assert_nil Region.from_edge_coordinates(10, 10, 11, 9) + end + + test "#== returns true when comparing with an identical Region" do + assert_equal Region.new(10, 10, 10, 10), Region.new(10, 10, 10, 10) + assert_not_equal Region.new(10, 10, 10, 10), Region.new(10, 10, 10, 11) + end + + test "#== returns true when comparing with equivalent Array of coordinates" do + assert_equal Region.new(10, 10, 10, 10), [10, 10, 10, 10] + assert_not_equal Region.new(10, 10, 10, 10), [10, 10, 10, 11] end end diff --git a/test/unit/registry_concurrency_test.rb b/test/unit/registry_concurrency_test.rb index 4553decd..44221ee8 100644 --- a/test/unit/registry_concurrency_test.rb +++ b/test/unit/registry_concurrency_test.rb @@ -1,115 +1,114 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" - -module CapybaraScreenshotDiff - # Guard #5 from the v2 core-redesign acceptance contract. - # - # Extends the single thread-isolation test (pending_screenshots_message) to - # the whole registry surface: add_assertion / verify / reset racing across - # threads must never leak assertions between threads. This is the regression - # net for parallel test runners and for D7's thread-local store fix. - class RegistryConcurrencyTest < ActiveSupport::TestCase - PassingCompare = Struct.new(:name) do - def different? = false - - def base_image_path = Pathname.new("/nonexistent/#{name}.base.png") - end - - def build_assertion(name) - assertion = SnapDiff::ScreenshotAssertion.new(name) - assertion.caller = ["#{name}:1"] - assertion.compare = PassingCompare.new(name) - assertion - end +require "snap_diff" + +# Guard #5 from the v2 core-redesign acceptance contract. +# +# Extends the single thread-isolation test (pending_screenshots_message) to +# the whole registry surface: add_assertion / verify / reset racing across +# threads must never leak assertions between threads. This is the regression +# net for parallel test runners and for D7's thread-local store fix. +class RegistryConcurrencyTest < ActiveSupport::TestCase + PassingCompare = Struct.new(:name) do + def different? = false + + def base_image_path = Pathname.new("/nonexistent/#{name}.base.png") + end - # ADR-008 step 6: SnapDiff.session is the canonical accessor and - # CapybaraScreenshotDiff.registry a forwarder over it -- they must hand - # back the *same* object, not two registries that happen to look alike. - test "SnapDiff.session and CapybaraScreenshotDiff.registry are the same object" do - assert_same SnapDiff.session, CapybaraScreenshotDiff.registry + def build_assertion(name) + assertion = SnapDiff::ScreenshotAssertion.new(name) + assertion.caller = ["#{name}:1"] + assertion.compare = PassingCompare.new(name) + assertion + end - SnapDiff.session.record_new_screenshot("shared_object_probe") - assert_equal ["shared_object_probe"], CapybaraScreenshotDiff.new_screenshots - ensure - SnapDiff.session.reset - end + # Memoized per fiber: repeated reads hand back the one registry the test + # is accumulating into, not a fresh empty one. + # (The identity of the v1 CapybaraScreenshotDiff.registry forwarder with + # this accessor is pinned in test/legacy/legacy_forwarders_test.rb.) + test "SnapDiff.session returns the same registry within a fiber" do + assert_same SnapDiff.session, SnapDiff.session + + SnapDiff.session.record_new_screenshot("shared_object_probe") + assert_equal ["shared_object_probe"], SnapDiff.session.new_screenshots + ensure + SnapDiff.session.reset + end - test "each thread gets its own registry instance" do - here = CapybaraScreenshotDiff.registry - there = Thread.new { CapybaraScreenshotDiff.registry }.value + test "each thread gets its own registry instance" do + here = SnapDiff.session + there = Thread.new { SnapDiff.session }.value - assert_not_same here, there - end + assert_not_same here, there + end - test "add_assertion, verify and reset in concurrent threads never leak across threads" do - threads = 2.times.map do |i| - Thread.new do - names = 25.times.map { |j| "thread_#{i}_shot_#{j}" } - observed = [] + test "add_assertion, verify and reset in concurrent threads never leak across threads" do + threads = 2.times.map do |i| + Thread.new do + names = 25.times.map { |j| "thread_#{i}_shot_#{j}" } + observed = [] - names.each do |name| - CapybaraScreenshotDiff.add_assertion(build_assertion(name)) - CapybaraScreenshotDiff.record_new_screenshot(name) - observed << CapybaraScreenshotDiff.assertions.map(&:name) - end + names.each do |name| + SnapDiff.session.add_assertion(build_assertion(name)) + SnapDiff.session.record_new_screenshot(name) + observed << SnapDiff.session.assertions.map(&:name) + end - CapybaraScreenshotDiff.verify # all compares pass -> must not raise + SnapDiff.session.verify # all compares pass -> must not raise - final_names = CapybaraScreenshotDiff.assertions.map(&:name) - new_screenshots = CapybaraScreenshotDiff.new_screenshots.dup + final_names = SnapDiff.session.assertions.map(&:name) + new_screenshots = SnapDiff.session.new_screenshots.dup - CapybaraScreenshotDiff.reset - after_reset = CapybaraScreenshotDiff.assertions.size + CapybaraScreenshotDiff.new_screenshots.size + SnapDiff.reset + after_reset = SnapDiff.session.assertions.size + SnapDiff.session.new_screenshots.size - {names: names, observed: observed, final_names: final_names, - new_screenshots: new_screenshots, after_reset: after_reset} - end + {names: names, observed: observed, final_names: final_names, + new_screenshots: new_screenshots, after_reset: after_reset} end + end - threads.map(&:value).each do |result| - # At every point each thread saw only its own assertions, in order. - result[:observed].each_with_index do |snapshot_of_names, index| - assert_equal result[:names].first(index + 1), snapshot_of_names - end - assert_equal result[:names], result[:final_names] - assert_equal result[:names], result[:new_screenshots] - assert_equal 0, result[:after_reset], "reset must clear only the calling thread's registry" + threads.map(&:value).each do |result| + # At every point each thread saw only its own assertions, in order. + result[:observed].each_with_index do |snapshot_of_names, index| + assert_equal result[:names].first(index + 1), snapshot_of_names end - - # The main thread's registry stayed untouched by the worker threads. - assert_not_predicate CapybaraScreenshotDiff.registry, :assertions_present? - assert_empty CapybaraScreenshotDiff.new_screenshots + assert_equal result[:names], result[:final_names] + assert_equal result[:names], result[:new_screenshots] + assert_equal 0, result[:after_reset], "reset must clear only the calling thread's registry" end - test "a failing assertion in one thread does not fail verify in another" do - failing_compare = Struct.new(:name) { - def different? = true + # The main thread's registry stayed untouched by the worker threads. + assert_not_predicate SnapDiff.session, :assertions_present? + assert_empty SnapDiff.session.new_screenshots + end - def error_message = "boom" - } + test "a failing assertion in one thread does not fail verify in another" do + failing_compare = Struct.new(:name) { + def different? = true - failing_thread = Thread.new do - assertion = SnapDiff::ScreenshotAssertion.new("failing_shot") - assertion.caller = ["failing_shot:1"] - assertion.compare = failing_compare.new("failing_shot") - CapybaraScreenshotDiff.add_assertion(assertion) + def error_message = "boom" + } - raised = assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) { CapybaraScreenshotDiff.verify } - CapybaraScreenshotDiff.registry.reset - raised - end + failing_thread = Thread.new do + assertion = SnapDiff::ScreenshotAssertion.new("failing_shot") + assertion.caller = ["failing_shot:1"] + assertion.compare = failing_compare.new("failing_shot") + SnapDiff.session.add_assertion(assertion) - passing_thread = Thread.new do - CapybaraScreenshotDiff.add_assertion(build_assertion("passing_shot")) - CapybaraScreenshotDiff.verify - CapybaraScreenshotDiff.registry.reset - :passed - end + raised = assert_raises(SnapDiff::ExpectationNotMet) { SnapDiff.session.verify } + SnapDiff.session.reset + raised + end - assert_match(/failing_shot/, failing_thread.value.message) - assert_equal :passed, passing_thread.value + passing_thread = Thread.new do + SnapDiff.session.add_assertion(build_assertion("passing_shot")) + SnapDiff.session.verify + SnapDiff.session.reset + :passed end + + assert_match(/failing_shot/, failing_thread.value.message) + assert_equal :passed, passing_thread.value end end diff --git a/test/unit/reporter_interplay_test.rb b/test/unit/reporter_interplay_test.rb index 132a69b5..d4667992 100644 --- a/test/unit/reporter_interplay_test.rb +++ b/test/unit/reporter_interplay_test.rb @@ -1,122 +1,120 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" - -module CapybaraScreenshotDiff - # Guard #4 from the v2 core-redesign acceptance contract. - # - # Reporter tests used to exercise the wrong seam (reporter.record called - # with hand-built arrays). This pins the real wiring: a delayed - # assert_matches_screenshot lands in the registry, CapybaraScreenshotDiff.reset - # notifies registered reporters BEFORE clearing the registry, and - # finalize_reporters! drives finalize/summary. - class ReporterInterplayTest < ActiveSupport::TestCase - include SnapDiff::DSL - include CapybaraScreenshotDiff::DSLStub - - class SpyReporter - attr_reader :recorded, :finalized - - def initialize - @recorded = [] - @finalized = false - end - - def record(assertions) - @recorded << assertions.dup - end - - def finalize - @finalized = true - end - - def summary - "spy reporter summary" - end +require "snap_diff" + +# Guard #4 from the v2 core-redesign acceptance contract. +# +# Reporter tests used to exercise the wrong seam (reporter.record called +# with hand-built arrays). This pins the real wiring: a delayed +# assert_matches_screenshot lands in the registry, SnapDiff.reset +# notifies registered reporters BEFORE clearing the registry, and +# finalize_reporters! drives finalize/summary. +class ReporterInterplayTest < ActiveSupport::TestCase + include SnapDiff::DSL + include DSLStub + + class SpyReporter + attr_reader :recorded, :finalized + + def initialize + @recorded = [] + @finalized = false end - setup do - @spy = SpyReporter.new - CapybaraScreenshotDiff.reporters_mutex.synchronize do - @original_reporters = CapybaraScreenshotDiff.reporters.dup - CapybaraScreenshotDiff.reporters.clear - CapybaraScreenshotDiff.reporters << @spy - end + def record(assertions) + @recorded << assertions.dup end - teardown do - CapybaraScreenshotDiff.reporters_mutex.synchronize do - CapybaraScreenshotDiff.reporters.clear - CapybaraScreenshotDiff.reporters.concat(@original_reporters) - end + def finalize + @finalized = true end - test "a delayed assertion reaches registered reporters through the real reset path" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :a) - - assert_matches_screenshot(snap.full_name) # delayed by default - assert_predicate CapybaraScreenshotDiff.registry, :assertions_present? - - CapybaraScreenshotDiff.reset + def summary + "spy reporter summary" + end + end - assert_equal 1, @spy.recorded.size, "reset must notify reporters with the pending assertions" - recorded_assertions = @spy.recorded.first - assert_equal [snap.full_name], recorded_assertions.map(&:name) + setup do + @spy = SpyReporter.new + SnapDiff::Reporting.mutex.synchronize do + @original_reporters = SnapDiff::Reporting.reporters.dup + SnapDiff::Reporting.reporters.clear + SnapDiff::Reporting.reporters << @spy + end + end - assert_not_predicate CapybaraScreenshotDiff.registry, :assertions_present?, - "reset must clear the registry only after reporters were notified" - end + teardown do + SnapDiff::Reporting.mutex.synchronize do + SnapDiff::Reporting.reporters.clear + SnapDiff::Reporting.reporters.concat(@original_reporters) end + end - test "reset without assertions does not notify reporters" do - CapybaraScreenshotDiff.reset + test "a delayed assertion reaches registered reporters through the real reset path" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :a) - assert_empty @spy.recorded - end + assert_matches_screenshot(snap.full_name) # delayed by default + assert_predicate SnapDiff.session, :assertions_present? - test "finalize_reporters! finalizes each reporter and prints its summary" do - assert_output(/spy reporter summary/) do - CapybaraScreenshotDiff.finalize_reporters! - end + SnapDiff.reset - assert @spy.finalized + assert_equal 1, @spy.recorded.size, "reset must notify reporters with the pending assertions" + recorded_assertions = @spy.recorded.first + assert_equal [snap.full_name], recorded_assertions.map(&:name) + + assert_not_predicate SnapDiff.session, :assertions_present?, + "reset must clear the registry only after reporters were notified" end + end - class RaisingReporter - def record(_assertions) = raise "record boom" + test "reset without assertions does not notify reporters" do + SnapDiff.reset - def finalize = raise "finalize boom" + assert_empty @spy.recorded + end - def summary = nil + test "finalize_reporters! finalizes each reporter and prints its summary" do + assert_output(/spy reporter summary/) do + SnapDiff::Reporting.finalize! end - test "a raising reporter during reset warns and does not stop other reporters" do - CapybaraScreenshotDiff.reporters_mutex.synchronize do - CapybaraScreenshotDiff.reporters.unshift(RaisingReporter.new) - end + assert @spy.finalized + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :a) - assert_matches_screenshot(snap.full_name) + class RaisingReporter + def record(_assertions) = raise "record boom" - _out, err = capture_io { CapybaraScreenshotDiff.reset } + def finalize = raise "finalize boom" - assert_match(/\[snap_diff\] Reporter \S*RaisingReporter failed \(RuntimeError: record boom\)/, err) - assert_equal 1, @spy.recorded.size, "reporters after the raising one must still be notified" - end + def summary = nil + end + + test "a raising reporter during reset warns and does not stop other reporters" do + SnapDiff::Reporting.mutex.synchronize do + SnapDiff::Reporting.reporters.unshift(RaisingReporter.new) end - test "a raising reporter during finalize_reporters! warns and does not stop other reporters" do - CapybaraScreenshotDiff.reporters_mutex.synchronize do - CapybaraScreenshotDiff.reporters.unshift(RaisingReporter.new) - end + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :a) + assert_matches_screenshot(snap.full_name) - _out, err = capture_io { CapybaraScreenshotDiff.finalize_reporters! } + _out, err = capture_io { SnapDiff.reset } - assert_match(/RaisingReporter failed \(RuntimeError: finalize boom\)/, err) - assert @spy.finalized, "reporters after the raising one must still be finalized" + assert_match(/\[snap_diff\] Reporter \S*RaisingReporter failed \(RuntimeError: record boom\)/, err) + assert_equal 1, @spy.recorded.size, "reporters after the raising one must still be notified" end end + + test "a raising reporter during finalize_reporters! warns and does not stop other reporters" do + SnapDiff::Reporting.mutex.synchronize do + SnapDiff::Reporting.reporters.unshift(RaisingReporter.new) + end + + _out, err = capture_io { SnapDiff::Reporting.finalize! } + + assert_match(/RaisingReporter failed \(RuntimeError: finalize boom\)/, err) + assert @spy.finalized, "reporters after the raising one must still be finalized" + end end diff --git a/test/unit/reporters/default_test.rb b/test/unit/reporters/default_test.rb index 6dd55129..ccff23fd 100644 --- a/test/unit/reporters/default_test.rb +++ b/test/unit/reporters/default_test.rb @@ -3,67 +3,65 @@ require "test_helper" require "snap_diff/reporters/default" -require "capybara/screenshot/diff/drivers/vips_driver" if defined?(Vips) +require "snap_diff/drivers/vips_driver" if defined?(Vips) -module Capybara::Screenshot::Diff - class Reporters::DefaultTest < ActiveSupport::TestCase - setup do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - @_tmpdir = Pathname.new(Dir.mktmpdir) - end +class DefaultReporterTest < ActiveSupport::TestCase + setup do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + @_tmpdir = Pathname.new(Dir.mktmpdir) + end - teardown do - FileUtils.remove_entry @_tmpdir if @_tmpdir - end + teardown do + FileUtils.remove_entry @_tmpdir if @_tmpdir + end - test "for vips driver generates heatmap diff file" do - driver = SnapDiff::Drivers::VipsDriver.new - comparison = build_comparison_for(driver, "a.png", "b.png") - reporter = SnapDiff::Reporters::Default.new(driver.find_difference_region(comparison)) + test "for vips driver generates heatmap diff file" do + driver = SnapDiff::Drivers::VipsDriver.new + comparison = build_comparison_for(driver, "a.png", "b.png") + reporter = SnapDiff::Reporters::Default.new(driver.find_difference_region(comparison)) - reporter.generate + reporter.generate - assert_same_images "a-and-b.heatmap.diff.png", reporter.heatmap_diff_path - end + assert_same_images "a-and-b.heatmap.diff.png", reporter.heatmap_diff_path + end - test "#clean_tmp_files removes heatmap diff along with other diff artifacts" do - driver = SnapDiff::Drivers::VipsDriver.new - comparison = build_comparison_for(driver, "a.png", "b.png") - reporter = SnapDiff::Reporters::Default.new(driver.find_difference_region(comparison)) - reporter.generate + test "#clean_tmp_files removes heatmap diff along with other diff artifacts" do + driver = SnapDiff::Drivers::VipsDriver.new + comparison = build_comparison_for(driver, "a.png", "b.png") + reporter = SnapDiff::Reporters::Default.new(driver.find_difference_region(comparison)) + reporter.generate - assert_predicate reporter.heatmap_diff_path, :exist? + assert_predicate reporter.heatmap_diff_path, :exist? - reporter.clean_tmp_files + reporter.clean_tmp_files - assert_not reporter.annotated_image_path.exist?, "diff should be cleaned" - assert_not reporter.annotated_base_image_path.exist?, "base diff should be cleaned" - assert_not reporter.heatmap_diff_path.exist?, "heatmap diff should be cleaned" - end + assert_not reporter.annotated_image_path.exist?, "diff should be cleaned" + assert_not reporter.annotated_base_image_path.exist?, "base diff should be cleaned" + assert_not reporter.heatmap_diff_path.exist?, "heatmap diff should be cleaned" + end - test "failure message reports metrics without leaking image objects" do - driver = SnapDiff::Drivers::VipsDriver.new - comparison = build_comparison_for(driver, "a.png", "b.png") - difference = driver.find_difference_region(comparison) - difference.meta[:difference_level] = 0.42 + test "failure message reports metrics without leaking image objects" do + driver = SnapDiff::Drivers::VipsDriver.new + comparison = build_comparison_for(driver, "a.png", "b.png") + difference = driver.find_difference_region(comparison) + difference.meta[:difference_level] = 0.42 - message = SnapDiff::Reporters::Default.new(difference).generate - metrics = message.lines.first + message = SnapDiff::Reporters::Default.new(difference).generate + metrics = message.lines.first - assert_includes metrics, "area_size" - assert_includes metrics, "region" - assert_includes metrics, "difference_level" - assert_not_includes metrics, "Vips::Image" - assert_not_includes metrics, "0x" - end + assert_includes metrics, "area_size" + assert_includes metrics, "region" + assert_includes metrics, "difference_level" + assert_not_includes metrics, "Vips::Image" + assert_not_includes metrics, "0x" + end - private + private - def build_comparison_for(driver, *images) - new_image = driver.from_file(TEST_IMAGES_DIR.join(images.first)) - base_image = driver.from_file(TEST_IMAGES_DIR.join(images.last)) + def build_comparison_for(driver, *images) + new_image = driver.from_file(TEST_IMAGES_DIR.join(images.first)) + base_image = driver.from_file(TEST_IMAGES_DIR.join(images.last)) - SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, @_tmpdir / images.first, @_tmpdir / images.last) - end + SnapDiff::Comparison::Images.new(new_image, base_image, {}, driver, @_tmpdir / images.first, @_tmpdir / images.last) end end diff --git a/test/unit/reporters/html_reporter_test.rb b/test/unit/reporters/html_reporter_test.rb index 5b44da72..f1ec2258 100644 --- a/test/unit/reporters/html_reporter_test.rb +++ b/test/unit/reporters/html_reporter_test.rb @@ -1,255 +1,251 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff/reporters/html" +require "snap_diff/reporters/html" -module CapybaraScreenshotDiff - module Reporters - class HTMLReporterTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub +class HTMLReporterTest < ActiveSupport::TestCase + include DSLStub - setup do - @output_dir = Pathname.new(Dir.mktmpdir) - @output_path = @output_dir / "report.html" - end + setup do + @output_dir = Pathname.new(Dir.mktmpdir) + @output_path = @output_dir / "report.html" + end - teardown do - FileUtils.remove_entry(@output_dir) - end + teardown do + FileUtils.remove_entry(@output_dir) + end - # Requiring this file is the only thing that puts a reporter in the - # default list -- nothing else in the suite registers one. Guards the - # auto-registration block at the bottom of reporters/html.rb, which - # goes through SnapDiff::Reporting.register since ADR-008 step 6. - test "requiring the reporter auto-registers exactly one HTML reporter" do - registered = SnapDiff::Reporting.reporters.count { |reporter| reporter.is_a?(SnapDiff::Reporters::HTML) } + # Requiring this file is the only thing that puts a reporter in the + # default list -- nothing else in the suite registers one. Guards the + # auto-registration block at the bottom of reporters/html.rb, which + # goes through SnapDiff::Reporting.register since ADR-008 step 6. + test "requiring the reporter auto-registers exactly one HTML reporter" do + registered = SnapDiff::Reporting.reporters.count { |reporter| reporter.is_a?(SnapDiff::Reporters::HTML) } - assert_equal 1, registered - end + assert_equal 1, registered + end - test "#record with no assertions writes nothing" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([]) + test "#record with no assertions writes nothing" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([]) - assert_not @output_path.exist? - end + assert_not @output_path.exist? + end - test "#record with passing assertions writes nothing" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + test "#record with passing assertions writes nothing" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_passing_assertion("index")]) - reporter.finalize + reporter.record([build_passing_assertion("index")]) + reporter.finalize - assert_not @output_path.exist? - end + assert_not @output_path.exist? + end - test "#record and #finalize with failing assertion generates HTML file" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + test "#record and #finalize with failing assertion generates HTML file" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_failing_assertion("index")]) - reporter.finalize + reporter.record([build_failing_assertion("index")]) + reporter.finalize - assert @output_path.exist? - html = @output_path.read - assert_includes html, "" - assert_includes html, "index" - end + assert @output_path.exist? + html = @output_path.read + assert_includes html, "" + assert_includes html, "index" + end - test "#record and #finalize includes summary stats" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + test "#record and #finalize includes summary stats" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([ - build_failing_assertion("page_a"), - build_passing_assertion("page_b"), - build_failing_assertion("page_c") - ]) - reporter.finalize + reporter.record([ + build_failing_assertion("page_a"), + build_passing_assertion("page_b"), + build_failing_assertion("page_c") + ]) + reporter.finalize - html = @output_path.read - assert_includes html, "2 failed" - assert_includes html, "1 passed" - assert_includes html, "3 total" - end - - test "#record tolerates broken assertions without crashing" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + html = @output_path.read + assert_includes html, "2 failed" + assert_includes html, "1 passed" + assert_includes html, "3 total" + end - broken = SnapDiff::ScreenshotAssertion.new("broken") - broken.compare = Object.new # will raise on .difference + test "#record tolerates broken assertions without crashing" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - valid = build_failing_assertion("valid") + broken = SnapDiff::ScreenshotAssertion.new("broken") + broken.compare = Object.new # will raise on .difference - assert_nothing_raised do - reporter.record([broken, valid]) - end + valid = build_failing_assertion("valid") - reporter.finalize - assert @output_path.exist? - assert_includes @output_path.read, "valid" - end + assert_nothing_raised do + reporter.record([broken, valid]) + end - test "HTML reporter defaults to screenshot root path" do - reporter = SnapDiff::Reporters::HTML.new - expected = Capybara::Screenshot.root / Capybara::Screenshot.save_path / "snap_diff_report.html" - assert_equal expected, reporter.output_path - end + reporter.finalize + assert @output_path.exist? + assert_includes @output_path.read, "valid" + end - test "#record uses relative paths by default" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + test "HTML reporter defaults to screenshot root path" do + reporter = SnapDiff::Reporters::HTML.new + expected = SnapDiff.config.root / SnapDiff.config.save_path / "snap_diff_report.html" + assert_equal expected, reporter.output_path + end - reporter.record([build_failing_assertion("rel")]) - reporter.finalize + test "#record uses relative paths by default" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - html = @output_path.read - assert_not_includes html, "data:image" - end + reporter.record([build_failing_assertion("rel")]) + reporter.finalize - test "#record embeds base64 images when embed_images: true" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path, embed_images: true) + html = @output_path.read + assert_not_includes html, "data:image" + end - reporter.record([build_failing_assertion("embed")]) - reporter.finalize + test "#record embeds base64 images when embed_images: true" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path, embed_images: true) - html = @output_path.read - assert_includes html, "data:image/png;base64," - end + reporter.record([build_failing_assertion("embed")]) + reporter.finalize - test "#record from multiple threads produces correct totals" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - threads = 10 - assertions_per_thread = 5 + html = @output_path.read + assert_includes html, "data:image/png;base64," + end - workers = threads.times.map do - Thread.new do - batch = assertions_per_thread.times.map { |i| build_passing_assertion("t#{Thread.current.object_id}_#{i}") } - reporter.record(batch) - end - end - workers.each(&:join) + test "#record from multiple threads produces correct totals" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + threads = 10 + assertions_per_thread = 5 - assert_equal threads * assertions_per_thread, reporter.total + workers = threads.times.map do + Thread.new do + batch = assertions_per_thread.times.map { |i| build_passing_assertion("t#{Thread.current.object_id}_#{i}") } + reporter.record(batch) end + end + workers.each(&:join) - test "#record and #finalize synchronize internal state" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + assert_equal threads * assertions_per_thread, reporter.total + end - fake_mutex = Class.new do - attr_reader :synchronize_calls + test "#record and #finalize synchronize internal state" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - def initialize - @synchronize_calls = 0 - end + fake_mutex = Class.new do + attr_reader :synchronize_calls - def synchronize - @synchronize_calls += 1 - yield - end - end.new + def initialize + @synchronize_calls = 0 + end - # Using private state and a fake mutex here is intentional. - # This test guards a tricky synchronization detail; avoid refactors unless behavior changes. - reporter.instance_variable_set(:@mutex, fake_mutex) + def synchronize + @synchronize_calls += 1 + yield + end + end.new - reporter.record([build_passing_assertion("sync")]) - reporter.finalize + # Using private state and a fake mutex here is intentional. + # This test guards a tricky synchronization detail; avoid refactors unless behavior changes. + reporter.instance_variable_set(:@mutex, fake_mutex) - assert_operator fake_mutex.synchronize_calls, :>=, 1 - end + reporter.record([build_passing_assertion("sync")]) + reporter.finalize - test "#finalize returns output_path when there are failures" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_failing_assertion("fail")]) - result = reporter.finalize + assert_operator fake_mutex.synchronize_calls, :>=, 1 + end - assert_instance_of Pathname, result - end + test "#finalize returns output_path when there are failures" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([build_failing_assertion("fail")]) + result = reporter.finalize - test "#finalize returns nil when no failures" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_passing_assertion("pass")]) - result = reporter.finalize + assert_instance_of Pathname, result + end - assert_nil result - end + test "#finalize returns nil when no failures" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([build_passing_assertion("pass")]) + result = reporter.finalize - test "#summary returns screenshot count and status" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_passing_assertion("ok"), build_failing_assertion("fail")]) - reporter.finalize + assert_nil result + end - summary = reporter.summary - assert_includes summary, "1 failure" - assert_includes summary, "2 screenshots" - assert_includes summary, @output_path.to_s - end + test "#summary returns screenshot count and status" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([build_passing_assertion("ok"), build_failing_assertion("fail")]) + reporter.finalize - test "#summary pluralizes failures label for multiple failures" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([ - build_failing_assertion("first failure"), - build_failing_assertion("second failure") - ]) - reporter.finalize - - summary = reporter.summary - assert_includes summary, "2 failures" - assert_includes summary, "2 screenshots" - assert_includes summary, @output_path.to_s - end + summary = reporter.summary + assert_includes summary, "1 failure" + assert_includes summary, "2 screenshots" + assert_includes summary, @output_path.to_s + end - test "#summary when all pass shows no failures" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_passing_assertion("ok")]) - reporter.finalize + test "#summary pluralizes failures label for multiple failures" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([ + build_failing_assertion("first failure"), + build_failing_assertion("second failure") + ]) + reporter.finalize + + summary = reporter.summary + assert_includes summary, "2 failures" + assert_includes summary, "2 screenshots" + assert_includes summary, @output_path.to_s + end - summary = reporter.summary - assert_includes summary, "1 screenshot" - assert_includes summary, "no failures" - refute_includes summary, @output_path.to_s - end + test "#summary when all pass shows no failures" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([build_passing_assertion("ok")]) + reporter.finalize - test "#summary when no screenshots recorded" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - assert_nil reporter.summary - end + summary = reporter.summary + assert_includes summary, "1 screenshot" + assert_includes summary, "no failures" + refute_includes summary, @output_path.to_s + end - test "#finalize can retry after write_report failure" do - reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) - reporter.record([build_failing_assertion("retry")]) + test "#summary when no screenshots recorded" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + assert_nil reporter.summary + end - # Make write_report fail on first attempt - FileUtils.rm_rf(@output_dir) - File.write(@output_dir.to_s, "not a directory") + test "#finalize can retry after write_report failure" do + reporter = SnapDiff::Reporters::HTML.new(output_path: @output_path) + reporter.record([build_failing_assertion("retry")]) - begin - reporter.finalize - rescue # rubocop:disable Lint/SuppressedException - end + # Make write_report fail on first attempt + FileUtils.rm_rf(@output_dir) + File.write(@output_dir.to_s, "not a directory") - # Restore writable directory and retry - File.delete(@output_dir.to_s) - FileUtils.mkdir_p(@output_dir) + begin + reporter.finalize + rescue # rubocop:disable Lint/SuppressedException + end - result = reporter.finalize - assert_instance_of Pathname, result - assert @output_path.exist? - end + # Restore writable directory and retry + File.delete(@output_dir.to_s) + FileUtils.mkdir_p(@output_dir) - private + result = reporter.finalize + assert_instance_of Pathname, result + assert @output_path.exist? + end - def build_passing_assertion(name) - compare = make_comparison(:a, :a, destination: "pass_#{name}") - compare.processed + private - SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } - end + def build_passing_assertion(name) + compare = make_comparison(:a, :a, destination: "pass_#{name}") + compare.processed + + SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } + end - def build_failing_assertion(name) - compare = make_comparison(:a, :b, destination: "fail_#{name}") - compare.processed + def build_failing_assertion(name) + compare = make_comparison(:a, :b, destination: "fail_#{name}") + compare.processed - SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } - end - end + SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } end end diff --git a/test/unit/reporters_mutex_test.rb b/test/unit/reporters_mutex_test.rb index b610e583..c6eba894 100644 --- a/test/unit/reporters_mutex_test.rb +++ b/test/unit/reporters_mutex_test.rb @@ -2,72 +2,70 @@ require "test_helper" -module CapybaraScreenshotDiff - class ReportersMutexTest < ActiveSupport::TestCase - setup do - @original_reporters = CapybaraScreenshotDiff.reporters.dup - CapybaraScreenshotDiff.reporters.clear - end - - teardown do - CapybaraScreenshotDiff.reporters.clear - CapybaraScreenshotDiff.reporters.concat(@original_reporters) - end +class ReportersMutexTest < ActiveSupport::TestCase + setup do + @original_reporters = SnapDiff::Reporting.reporters.dup + SnapDiff::Reporting.reporters.clear + end - test "reporters_mutex is eagerly initialized" do - assert_instance_of Mutex, CapybaraScreenshotDiff.reporters_mutex - end + teardown do + SnapDiff::Reporting.reporters.clear + SnapDiff::Reporting.reporters.concat(@original_reporters) + end - test "reporters_mutex returns the same instance" do - assert_same CapybaraScreenshotDiff.reporters_mutex, CapybaraScreenshotDiff.reporters_mutex - end + test "reporters_mutex is eagerly initialized" do + assert_instance_of Mutex, SnapDiff::Reporting.mutex + end - # ADR-008 step 6: SnapDiff::Reporting.register is the canonical way in; - # CapybaraScreenshotDiff.reporters stays as the compat view of the same - # array, so a registration must be visible through both. - test "register appends to the array CapybaraScreenshotDiff.reporters exposes" do - reporter = Object.new + test "reporters_mutex returns the same instance" do + assert_same SnapDiff::Reporting.mutex, SnapDiff::Reporting.mutex + end - assert_same reporter, SnapDiff::Reporting.register(reporter) - assert_same SnapDiff::Reporting.reporters, CapybaraScreenshotDiff.reporters - assert_includes CapybaraScreenshotDiff.reporters, reporter - end + # ADR-008 step 6: SnapDiff::Reporting.register is the canonical way in, + # and it returns the reporter it registered. + # (That the v1 CapybaraScreenshotDiff.reporters view is the SAME array is + # pinned in test/legacy/legacy_forwarders_test.rb.) + test "register appends to the array SnapDiff::Reporting.reporters exposes" do + reporter = Object.new - # Best-effort probe: MRI's GVL can make an unsynchronized Array#<< look - # safe, so a green run here is not proof. The mutex in .register is the - # actual defense (issue #217 item 2); this pins that no registration is - # dropped under contention. - test "concurrent register calls retain every reporter" do - reporters = 32.times.map { Object.new } + assert_same reporter, SnapDiff::Reporting.register(reporter) + assert_includes SnapDiff::Reporting.reporters, reporter + end - reporters.map { |reporter| Thread.new { SnapDiff::Reporting.register(reporter) } }.each(&:join) + # Best-effort probe: MRI's GVL can make an unsynchronized Array#<< look + # safe, so a green run here is not proof. The mutex in .register is the + # actual defense (issue #217 item 2); this pins that no registration is + # dropped under contention. + test "concurrent register calls retain every reporter" do + reporters = 32.times.map { Object.new } - assert_equal reporters.size, SnapDiff::Reporting.reporters.size - assert_empty reporters - SnapDiff::Reporting.reporters - end + reporters.map { |reporter| Thread.new { SnapDiff::Reporting.register(reporter) } }.each(&:join) - test "reporters notification iterates over snapshot" do - received = [] + assert_equal reporters.size, SnapDiff::Reporting.reporters.size + assert_empty reporters - SnapDiff::Reporting.reporters + end - mutating_reporter = Class.new do - define_method :record do |assertions| - received << [:original, assertions] - CapybaraScreenshotDiff.reporters.clear - CapybaraScreenshotDiff.reporters << Class.new { - define_method(:record) { |a| received << [:added, a] } - }.new - end - end.new + test "reporters notification iterates over snapshot" do + received = [] - CapybaraScreenshotDiff.reporters << mutating_reporter + mutating_reporter = Class.new do + define_method :record do |assertions| + received << [:original, assertions] + SnapDiff::Reporting.reporters.clear + SnapDiff::Reporting.reporters << Class.new { + define_method(:record) { |a| received << [:added, a] } + }.new + end + end.new - assertions = [:some, :assertions] + SnapDiff::Reporting.reporters << mutating_reporter - assert_nothing_raised do - CapybaraScreenshotDiff.send(:notify_reporters, assertions) - end + assertions = [:some, :assertions] - assert_equal [[:original, assertions]], received + assert_nothing_raised do + CapybaraScreenshotDiff.send(:notify_reporters, assertions) end + + assert_equal [[:original, assertions]], received end end diff --git a/test/unit/screenshot_assertion_test.rb b/test/unit/screenshot_assertion_test.rb index 2aabbcd3..b4802289 100644 --- a/test/unit/screenshot_assertion_test.rb +++ b/test/unit/screenshot_assertion_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" +require "snap_diff" module SnapDiff # Pins the baseline-archiving side effect of the verify flow: when a @@ -11,7 +11,7 @@ module SnapDiff # extracted from the read path into an explicit archive step. class ScreenshotAssertionTest < ActiveSupport::TestCase include SnapDiff::DSL - include CapybaraScreenshotDiff::DSLStub + include DSLStub test "#validate! archives the baseline when the comparison passes" do comparison = make_comparison(:a, :a) @@ -27,7 +27,7 @@ class ScreenshotAssertionTest < ActiveSupport::TestCase comparison = make_comparison(:a, :b) assertion = build_assertion(comparison) - assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) { assertion.validate! } + assert_raises(SnapDiff::ExpectationNotMet) { assertion.validate! } assert comparison.base_image_path.exist?, "base image must be kept for the reporter on failure" end @@ -39,7 +39,7 @@ class ScreenshotAssertionTest < ActiveSupport::TestCase assert_matches_screenshot(snap.full_name) # delayed by default assert comparison_for(snap.full_name).base_image_path.exist?, "verify has not run yet: base image must still be present" - CapybaraScreenshotDiff.verify + SnapDiff.session.verify assert_not snap.base_path.exist?, "verify must archive the baseline of a passing assertion" assert_predicate snap.path, :exist? @@ -52,7 +52,7 @@ class ScreenshotAssertionTest < ActiveSupport::TestCase assert_matches_screenshot(snap.full_name) # delayed by default - assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) { CapybaraScreenshotDiff.verify } + assert_raises(SnapDiff::ExpectationNotMet) { SnapDiff.session.verify } assert snap.base_path.exist?, "base image must be kept for the reporter on failure" end @@ -115,7 +115,7 @@ def build_assertion(comparison, name: "name") end def comparison_for(name) - CapybaraScreenshotDiff.assertions.find { |assertion| assertion.name == name }.compare + SnapDiff.session.assertions.find { |assertion| assertion.name == name }.compare end end end diff --git a/test/unit/screenshot_matcher_test.rb b/test/unit/screenshot_matcher_test.rb index bd60c579..90ccebb9 100644 --- a/test/unit/screenshot_matcher_test.rb +++ b/test/unit/screenshot_matcher_test.rb @@ -1,223 +1,217 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" - -module Capybara - module Screenshot - module Diff - # Guard #1 from the v2 core-redesign acceptance contract: pins - # ScreenshotMatcher's current external behavior (D1-D4) in isolation - # before the orchestration middle is redesigned. Until now the class had - # no dedicated unit test at all — it was exercised only indirectly - # through dsl_test stubs and full-browser integration tests. - class ScreenshotMatcherTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - - # A screenshoter probe that records the (capture_options, - # comparison_options) split it was built with (D1) and writes a real - # file so the rest of the flow proceeds. - def recording_screenshoter(calls) - Class.new do - define_method(:initialize) do |capture_options, comparison_options| - calls << [capture_options, comparison_options] - end - - def take_comparison_screenshot(snapshot) - snapshot.path.dirname.mkpath - FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) - end - end - end +require "snap_diff" + +# Guard #1 from the v2 core-redesign acceptance contract: pins +# ScreenshotMatcher's current external behavior (D1-D4) in isolation +# before the orchestration middle is redesigned. Until now the class had +# no dedicated unit test at all — it was exercised only indirectly +# through dsl_test stubs and full-browser integration tests. +class ScreenshotMatcherTest < ActiveSupport::TestCase + include DSLStub + + # A screenshoter probe that records the (capture_options, + # comparison_options) split it was built with (D1) and writes a real + # file so the rest of the flow proceeds. + def recording_screenshoter(calls) + Class.new do + define_method(:initialize) do |capture_options, comparison_options| + calls << [capture_options, comparison_options] + end - # D4: dual return shape — nil for the new-screenshot path, after - # side-effecting record_new_screenshot into the registry. - test "#build_screenshot_assertion returns nil and records a new screenshot when no baseline exists" do - # ScreenshoterStub resolves "c_" to the c.png fixture. - name = "c_#{Time.now.nsec}" + def take_comparison_screenshot(snapshot) + snapshot.path.dirname.mkpath + FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) + end + end + end - SnapDiff::Vcs.stub(:checkout_vcs, false) do - assertion = SnapDiff::ScreenshotMatcher.new(name).build_screenshot_assertion + # D4: dual return shape — nil for the new-screenshot path, after + # side-effecting record_new_screenshot into the registry. + test "#build_screenshot_assertion returns nil and records a new screenshot when no baseline exists" do + # ScreenshoterStub resolves "c_" to the c.png fixture. + name = "c_#{Time.now.nsec}" - assert_nil assertion - assert_includes CapybaraScreenshotDiff.new_screenshots, name - assert_predicate SnapDiff::SnapManager.path_for(name).path, :exist?, - "the screenshot is still captured on the new-screenshot path" - end - end + SnapDiff::Vcs.stub(:checkout_vcs, false) do + assertion = SnapDiff::ScreenshotMatcher.new(name).build_screenshot_assertion - # D4: the other return shape — a fully wired ScreenshotAssertion. - test "#build_screenshot_assertion returns an assertion with compare and caller when a baseline exists" do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - snap = create_snapshot_for(:a, :c) + assert_nil assertion + assert_includes SnapDiff.session.new_screenshots, name + assert_predicate SnapDiff::SnapManager.path_for(name).path, :exist?, + "the screenshot is still captured on the new-screenshot path" + end + end - assertion = SnapDiff::ScreenshotMatcher.new(snap.full_name).build_screenshot_assertion + # D4: the other return shape — a fully wired ScreenshotAssertion. + test "#build_screenshot_assertion returns an assertion with compare and caller when a baseline exists" do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + snap = create_snapshot_for(:a, :c) - assert_instance_of SnapDiff::ScreenshotAssertion, assertion - assert_equal snap.full_name, assertion.name - assert_kind_of Array, assertion.caller - assert_match(/screenshot_matcher_test\.rb/, assertion.caller.first) - assert_equal snap.path, assertion.compare.image_path - assert_equal snap.base_path, assertion.compare.base_image_path - end - end + assertion = SnapDiff::ScreenshotMatcher.new(snap.full_name).build_screenshot_assertion - # D1: the one-hash-carved-into-two option split. Stability/wait/crop - # are deleted into capture options; whatever is left over becomes the - # comparison options. - test "#build_screenshot_assertion splits capture options from comparison options" do - calls = [] + assert_instance_of SnapDiff::ScreenshotAssertion, assertion + assert_equal snap.full_name, assertion.name + assert_kind_of Array, assertion.caller + assert_match(/screenshot_matcher_test\.rb/, assertion.caller.first) + assert_equal snap.path, assertion.compare.image_path + assert_equal snap.base_path, assertion.compare.base_image_path + end + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff.config.stub(:screenshoter, recording_screenshoter(calls)) do - snap = create_snapshot_for(:a, :c) + # D1: the one-hash-carved-into-two option split. Stability/wait/crop + # are deleted into capture options; whatever is left over becomes the + # comparison options. + test "#build_screenshot_assertion splits capture options from comparison options" do + calls = [] - SnapDiff::ScreenshotMatcher.new(snap.full_name, tolerance: 0.03, wait: 5).build_screenshot_assertion - end - end + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff.config.stub(:screenshoter, recording_screenshoter(calls)) do + snap = create_snapshot_for(:a, :c) - assert_equal 1, calls.size - capture_options, comparison_options = calls.first + SnapDiff::ScreenshotMatcher.new(snap.full_name, tolerance: 0.03, wait: 5).build_screenshot_assertion + end + end - assert_equal 5, capture_options[:wait] - assert_nil capture_options[:stability_time_limit] - assert_includes capture_options, :crop - assert_includes capture_options, :screenshot_format + assert_equal 1, calls.size + capture_options, comparison_options = calls.first - assert_equal 0.03, comparison_options[:tolerance] - assert_not_includes comparison_options, :wait, "wait must be carved out of comparison options" - assert_not_includes comparison_options, :stability_time_limit - assert_not_includes comparison_options, :crop - end + assert_equal 5, capture_options[:wait] + assert_nil capture_options[:stability_time_limit] + assert_includes capture_options, :crop + assert_includes capture_options, :screenshot_format - # D1 (fixed): the split is a pure partition — the input hash survives - # untouched. The frozen input would raise FrozenError under the old - # delete-based carve. - test "#extract_capture_and_comparison_options does not mutate the input options" do - matcher = SnapDiff::ScreenshotMatcher.new("a") - options = {tolerance: 0.03, wait: 5, crop: [0, 0, 2, 2], stability_time_limit: 1}.freeze + assert_equal 0.03, comparison_options[:tolerance] + assert_not_includes comparison_options, :wait, "wait must be carved out of comparison options" + assert_not_includes comparison_options, :stability_time_limit + assert_not_includes comparison_options, :crop + end - capture_options, comparison_options = matcher.send(:extract_capture_and_comparison_options, options) + # D1 (fixed): the split is a pure partition — the input hash survives + # untouched. The frozen input would raise FrozenError under the old + # delete-based carve. + test "#extract_capture_and_comparison_options does not mutate the input options" do + matcher = SnapDiff::ScreenshotMatcher.new("a") + options = {tolerance: 0.03, wait: 5, crop: [0, 0, 2, 2], stability_time_limit: 1}.freeze - assert_equal 5, capture_options[:wait] - assert_not_includes comparison_options, :wait - assert_equal({tolerance: 0.03, wait: 5, crop: [0, 0, 2, 2], stability_time_limit: 1}, options) - end + capture_options, comparison_options = matcher.send(:extract_capture_and_comparison_options, options) - # D2: screenshoter selection is by hash-key presence — no - # :stability_time_limit means the configured plain screenshoter. - test "#build_screenshot_assertion uses the configured screenshoter without stability_time_limit" do - calls = [] + assert_equal 5, capture_options[:wait] + assert_not_includes comparison_options, :wait + assert_equal({tolerance: 0.03, wait: 5, crop: [0, 0, 2, 2], stability_time_limit: 1}, options) + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff.config.stub(:screenshoter, recording_screenshoter(calls)) do - snap = create_snapshot_for(:a, :c) - SnapDiff::ScreenshotMatcher.new(snap.full_name).build_screenshot_assertion - end - end + # D2: screenshoter selection is by hash-key presence — no + # :stability_time_limit means the configured plain screenshoter. + test "#build_screenshot_assertion uses the configured screenshoter without stability_time_limit" do + calls = [] - assert_equal 1, calls.size, "the configured plain screenshoter must take the shot" - end + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff.config.stub(:screenshoter, recording_screenshoter(calls)) do + snap = create_snapshot_for(:a, :c) + SnapDiff::ScreenshotMatcher.new(snap.full_name).build_screenshot_assertion + end + end - # D2: presence of :stability_time_limit switches to StableScreenshoter. - test "#build_screenshot_assertion uses StableScreenshoter when stability_time_limit is present" do - stable_calls = [] - fake_stable = Object.new - def fake_stable.take_comparison_screenshot(snapshot) - snapshot.path.dirname.mkpath - FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) - end - - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff::StableScreenshoter.stub(:new, lambda { |capture_options, comparison_options| - stable_calls << [capture_options, comparison_options] - fake_stable - }) do - snap = create_snapshot_for(:a, :c) - SnapDiff::ScreenshotMatcher.new(snap.full_name, stability_time_limit: 0.1, wait: 1).build_screenshot_assertion - end - end - - assert_equal 1, stable_calls.size - assert_equal 0.1, stable_calls.first.first[:stability_time_limit] - end + assert_equal 1, calls.size, "the configured plain screenshoter must take the shot" + end - # The raise-only window-size guard (relocated in the redesign into - # Capture::Viewport#prepare!): wrong window size fails fast, before - # any capture. - test "#build_screenshot_assertion raises WindowSizeMismatchError when the window size is wrong" do - SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, true) do - SnapDiff::BrowserHelpers.stub(:selenium?, false) do - assert_raises(CapybaraScreenshotDiff::WindowSizeMismatchError) do - SnapDiff::ScreenshotMatcher.new("matcher_window_size").build_screenshot_assertion - end - end - end - - refute SnapDiff::SnapManager.path_for("matcher_window_size").path.exist?, - "no screenshot may be written when the window size is wrong" - end + # D2: presence of :stability_time_limit switches to StableScreenshoter. + test "#build_screenshot_assertion uses StableScreenshoter when stability_time_limit is present" do + stable_calls = [] + fake_stable = Object.new + def fake_stable.take_comparison_screenshot(snapshot) + snapshot.path.dirname.mkpath + FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) + end + + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff::StableScreenshoter.stub(:new, lambda { |capture_options, comparison_options| + stable_calls << [capture_options, comparison_options] + fake_stable + }) do + snap = create_snapshot_for(:a, :c) + SnapDiff::ScreenshotMatcher.new(snap.full_name, stability_time_limit: 0.1, wait: 1).build_screenshot_assertion + end + end - # Same guard on the compare-free #capture path. - test "#capture raises WindowSizeMismatchError when the window size is wrong" do - SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, true) do - SnapDiff::BrowserHelpers.stub(:selenium?, false) do - assert_raises(CapybaraScreenshotDiff::WindowSizeMismatchError) do - SnapDiff::ScreenshotMatcher.new("matcher_window_size").capture - end - end - end - - refute SnapDiff::SnapManager.path_for("matcher_window_size").path.exist?, - "no screenshot may be written when the window size is wrong" + assert_equal 1, stable_calls.size + assert_equal 0.1, stable_calls.first.first[:stability_time_limit] + end + + # The raise-only window-size guard (relocated in the redesign into + # Capture::Viewport#prepare!): wrong window size fails fast, before + # any capture. + test "#build_screenshot_assertion raises WindowSizeMismatchError when the window size is wrong" do + SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, true) do + SnapDiff::BrowserHelpers.stub(:selenium?, false) do + assert_raises(SnapDiff::WindowSizeMismatchError) do + SnapDiff::ScreenshotMatcher.new("matcher_window_size").build_screenshot_assertion end + end + end + + refute SnapDiff::SnapManager.path_for("matcher_window_size").path.exist?, + "no screenshot may be written when the window size is wrong" + end - # Pins ScreenshotMatcher's viewport-preparation cadence: exactly one - # window-size check per capture, before the screenshoter runs. The - # stable screenshoter is stubbed here, so this guard does not police - # the real retry loop — that it stays check-free is verified by - # reading (no window-size calls in stable_screenshoter.rb). - test "window size is checked exactly once per capture even when stability retries happen" do - checks = 0 - fake_stable = Object.new - def fake_stable.take_comparison_screenshot(snapshot) - # Simulates a stability loop that needed several attempts; nothing - # here may trigger another window-size check. - 2.times { snapshot.next_attempt_path! } - snapshot.path.dirname.mkpath - FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) - end - - SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, proc { |_expected| - checks += 1 - false - }) do - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff::StableScreenshoter.stub(:new, ->(*, **) { fake_stable }) do - snap = create_snapshot_for(:a, :c) - SnapDiff::ScreenshotMatcher.new(snap.full_name, stability_time_limit: 0.1, wait: 1).build_screenshot_assertion - end - end - end - - assert_equal 1, checks + # Same guard on the compare-free #capture path. + test "#capture raises WindowSizeMismatchError when the window size is wrong" do + SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, true) do + SnapDiff::BrowserHelpers.stub(:selenium?, false) do + assert_raises(SnapDiff::WindowSizeMismatchError) do + SnapDiff::ScreenshotMatcher.new("matcher_window_size").capture end + end + end - # #capture is the compare-free path: file written, no assertion built, - # nothing recorded in the registry. - test "#capture writes the screenshot without touching the registry" do - # ScreenshoterStub resolves "b_" to the b.png fixture. - name = "b_#{Time.now.nsec}" + refute SnapDiff::SnapManager.path_for("matcher_window_size").path.exist?, + "no screenshot may be written when the window size is wrong" + end - SnapDiff::Vcs.stub(:checkout_vcs, true) do - SnapDiff::ScreenshotMatcher.new(name).capture + # Pins ScreenshotMatcher's viewport-preparation cadence: exactly one + # window-size check per capture, before the screenshoter runs. The + # stable screenshoter is stubbed here, so this guard does not police + # the real retry loop — that it stays check-free is verified by + # reading (no window-size calls in stable_screenshoter.rb). + test "window size is checked exactly once per capture even when stability retries happen" do + checks = 0 + fake_stable = Object.new + def fake_stable.take_comparison_screenshot(snapshot) + # Simulates a stability loop that needed several attempts; nothing + # here may trigger another window-size check. + 2.times { snapshot.next_attempt_path! } + snapshot.path.dirname.mkpath + FileUtils.cp(File.expand_path("a.png", TEST_IMAGES_DIR), snapshot.path) + end - assert_predicate SnapDiff::SnapManager.path_for(name).path, :exist? - assert_not_predicate CapybaraScreenshotDiff.registry, :assertions_present? - assert_empty CapybaraScreenshotDiff.new_screenshots - end + SnapDiff::BrowserHelpers.stub(:window_size_is_wrong?, proc { |_expected| + checks += 1 + false + }) do + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff::StableScreenshoter.stub(:new, ->(*, **) { fake_stable }) do + snap = create_snapshot_for(:a, :c) + SnapDiff::ScreenshotMatcher.new(snap.full_name, stability_time_limit: 0.1, wait: 1).build_screenshot_assertion end end end + + assert_equal 1, checks + end + + # #capture is the compare-free path: file written, no assertion built, + # nothing recorded in the registry. + test "#capture writes the screenshot without touching the registry" do + # ScreenshoterStub resolves "b_" to the b.png fixture. + name = "b_#{Time.now.nsec}" + + SnapDiff::Vcs.stub(:checkout_vcs, true) do + SnapDiff::ScreenshotMatcher.new(name).capture + + assert_predicate SnapDiff::SnapManager.path_for(name).path, :exist? + assert_not_predicate SnapDiff.session, :assertions_present? + assert_empty SnapDiff.session.new_screenshots + end end end diff --git a/test/unit/screenshot_namer_test.rb b/test/unit/screenshot_namer_test.rb index 3810dd1a..741e4d01 100644 --- a/test/unit/screenshot_namer_test.rb +++ b/test/unit/screenshot_namer_test.rb @@ -2,98 +2,96 @@ require "test_helper" -module CapybaraScreenshotDiff - class ScreenshotNamerTest < ActiveSupport::TestCase - setup do - @screenshot_namer = SnapDiff::ScreenshotNamer.new - end - - test "#group= resets counter when group changes" do - @screenshot_namer.group = "group1" - assert_equal "group1/00_image", @screenshot_namer.full_name("image") - assert_equal "group1/01_image", @screenshot_namer.full_name("image") - - @screenshot_namer.group = "group2" - assert_equal "group2/00_image", @screenshot_namer.full_name("image") - end - - test "#group= handles nil group" do - @screenshot_namer.group = nil - assert_equal "image", @screenshot_namer.full_name("image") - assert_equal [], @screenshot_namer.directory_parts - end - - test "#group= handles empty string group" do - @screenshot_namer.group = "" - assert_equal "image", @screenshot_namer.full_name("image") - assert_equal [], @screenshot_namer.directory_parts - end - - test "#section= handles nil section" do - @screenshot_namer.section = nil - assert_equal [], @screenshot_namer.directory_parts - end - - test "#section= handles empty string section" do - @screenshot_namer.section = "" - assert_equal [], @screenshot_namer.directory_parts - end - - test "#full_name generates basic name when no group is set" do - assert_equal "image_a", @screenshot_namer.full_name("image_a") - assert_equal "image_b", @screenshot_namer.full_name("image_b") - end - - test "#full_name generates prefixed and incremented names when group is set" do - @screenshot_namer.group = "user_flow" - assert_equal "user_flow/00_step1", @screenshot_namer.full_name("step1") - assert_equal "user_flow/01_step2", @screenshot_namer.full_name("step2") - end - - test "#full_name handles symbol base_name and group" do - @screenshot_namer.group = "symbols" - assert_equal "symbols/00_my_symbol", @screenshot_namer.full_name(:my_symbol) - @screenshot_namer.group = nil - assert_equal "plain_symbol", @screenshot_namer.full_name(:plain_symbol) - end - - test "#full_name includes section and group" do - @screenshot_namer.section = "user_profile" - @screenshot_namer.group = "avatar_upload" - assert_equal File.join("user_profile", "avatar_upload", "00_new_image"), @screenshot_namer.full_name("new_image") - end - - test "#full_name adds counter for duplicated names with active group" do - @screenshot_namer.group = "user_flow" - assert_equal "user_flow/00_step1", @screenshot_namer.full_name("step1") - assert_equal "user_flow/01_step1", @screenshot_namer.full_name("step1") - assert_equal "user_flow/02_step1", @screenshot_namer.full_name("step1") - end - - test "#full_name ignores duplicate names without active group" do - @screenshot_namer.group = nil - assert_equal "step1", @screenshot_namer.full_name("step1") - assert_equal "step1", @screenshot_namer.full_name("step1") - end - - test "#directory_parts is empty initially" do - assert_equal [], @screenshot_namer.directory_parts - end - - test "#directory_parts contains section when set" do - @screenshot_namer.section = "s1" - assert_equal ["s1"], @screenshot_namer.directory_parts - end - - test "#directory_parts contains group when set" do - @screenshot_namer.group = "g1" - assert_equal ["g1"], @screenshot_namer.directory_parts - end - - test "#directory_parts contains section and group when both set" do - @screenshot_namer.section = "s1" - @screenshot_namer.group = "g1" - assert_equal ["s1", "g1"], @screenshot_namer.directory_parts - end +class ScreenshotNamerTest < ActiveSupport::TestCase + setup do + @screenshot_namer = SnapDiff::ScreenshotNamer.new + end + + test "#group= resets counter when group changes" do + @screenshot_namer.group = "group1" + assert_equal "group1/00_image", @screenshot_namer.full_name("image") + assert_equal "group1/01_image", @screenshot_namer.full_name("image") + + @screenshot_namer.group = "group2" + assert_equal "group2/00_image", @screenshot_namer.full_name("image") + end + + test "#group= handles nil group" do + @screenshot_namer.group = nil + assert_equal "image", @screenshot_namer.full_name("image") + assert_equal [], @screenshot_namer.directory_parts + end + + test "#group= handles empty string group" do + @screenshot_namer.group = "" + assert_equal "image", @screenshot_namer.full_name("image") + assert_equal [], @screenshot_namer.directory_parts + end + + test "#section= handles nil section" do + @screenshot_namer.section = nil + assert_equal [], @screenshot_namer.directory_parts + end + + test "#section= handles empty string section" do + @screenshot_namer.section = "" + assert_equal [], @screenshot_namer.directory_parts + end + + test "#full_name generates basic name when no group is set" do + assert_equal "image_a", @screenshot_namer.full_name("image_a") + assert_equal "image_b", @screenshot_namer.full_name("image_b") + end + + test "#full_name generates prefixed and incremented names when group is set" do + @screenshot_namer.group = "user_flow" + assert_equal "user_flow/00_step1", @screenshot_namer.full_name("step1") + assert_equal "user_flow/01_step2", @screenshot_namer.full_name("step2") + end + + test "#full_name handles symbol base_name and group" do + @screenshot_namer.group = "symbols" + assert_equal "symbols/00_my_symbol", @screenshot_namer.full_name(:my_symbol) + @screenshot_namer.group = nil + assert_equal "plain_symbol", @screenshot_namer.full_name(:plain_symbol) + end + + test "#full_name includes section and group" do + @screenshot_namer.section = "user_profile" + @screenshot_namer.group = "avatar_upload" + assert_equal File.join("user_profile", "avatar_upload", "00_new_image"), @screenshot_namer.full_name("new_image") + end + + test "#full_name adds counter for duplicated names with active group" do + @screenshot_namer.group = "user_flow" + assert_equal "user_flow/00_step1", @screenshot_namer.full_name("step1") + assert_equal "user_flow/01_step1", @screenshot_namer.full_name("step1") + assert_equal "user_flow/02_step1", @screenshot_namer.full_name("step1") + end + + test "#full_name ignores duplicate names without active group" do + @screenshot_namer.group = nil + assert_equal "step1", @screenshot_namer.full_name("step1") + assert_equal "step1", @screenshot_namer.full_name("step1") + end + + test "#directory_parts is empty initially" do + assert_equal [], @screenshot_namer.directory_parts + end + + test "#directory_parts contains section when set" do + @screenshot_namer.section = "s1" + assert_equal ["s1"], @screenshot_namer.directory_parts + end + + test "#directory_parts contains group when set" do + @screenshot_namer.group = "g1" + assert_equal ["g1"], @screenshot_namer.directory_parts + end + + test "#directory_parts contains section and group when both set" do + @screenshot_namer.section = "s1" + @screenshot_namer.group = "g1" + assert_equal ["s1", "g1"], @screenshot_namer.directory_parts end end diff --git a/test/unit/screenshot_test.rb b/test/unit/screenshot_test.rb index 6afbd270..04eb4cbb 100644 --- a/test/unit/screenshot_test.rb +++ b/test/unit/screenshot_test.rb @@ -3,26 +3,24 @@ require "test_helper" require "minitest/mock" -module Capybara - class ScreenshotTest < ActiveSupport::TestCase - test "SnapManager.root returns an absolute path" do - assert SnapDiff::SnapManager.root.absolute? - end +class ScreenshotTest < ActiveSupport::TestCase + test "SnapManager.root returns an absolute path" do + assert SnapDiff::SnapManager.root.absolute? + end - test "Screenshot.root returns a Pathname when Rails.root is a Pathname" do - # NOTE: We test that Rails.root is Pathname, which is true. - assert_kind_of Pathname, Capybara::Screenshot.root - assert Capybara::Screenshot.root.absolute? - end + test "Screenshot.root returns a Pathname when Rails.root is a Pathname" do + # NOTE: We test that Rails.root is Pathname, which is true. + assert_kind_of Pathname, SnapDiff.config.root + assert SnapDiff.config.root.absolute? + end - test "Screenshot.root can be set to a relative path and is converted to absolute" do - @orig_root = Capybara::Screenshot.root + test "Screenshot.root can be set to a relative path and is converted to absolute" do + @orig_root = SnapDiff.config.root - Capybara::Screenshot.root = "./tmp" - assert_kind_of Pathname, Capybara::Screenshot.root - assert Capybara::Screenshot.root.absolute? - ensure - Capybara::Screenshot.root = @orig_root if @orig_root - end + SnapDiff.config.root = "./tmp" + assert_kind_of Pathname, SnapDiff.config.root + assert SnapDiff.config.root.absolute? + ensure + SnapDiff.config.root = @orig_root if @orig_root end end diff --git a/test/unit/screenshoter_test.rb b/test/unit/screenshoter_test.rb index 0f156f99..8012c106 100644 --- a/test/unit/screenshoter_test.rb +++ b/test/unit/screenshoter_test.rb @@ -3,62 +3,58 @@ require "test_helper" require "minitest/mock" -module Capybara - module Screenshot - class ScreenshoterTest < ActiveSupport::TestCase - include SnapDiff::DSL - include CapybaraScreenshotDiff::DSLStub +class ScreenshoterTest < ActiveSupport::TestCase + include SnapDiff::DSL + include DSLStub - test "#take_screenshot without wait skips image loading" do - screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png}) + test "#take_screenshot without wait skips image loading" do + screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png}) - mock = ::Minitest::Mock.new - mock.expect(:save_screenshot, true) { |path| path.include?("01_a.png") } + mock = ::Minitest::Mock.new + mock.expect(:save_screenshot, true) { |path| path.include?("01_a.png") } - SnapDiff::BrowserHelpers.stub(:session, mock) do - screenshoter.stub(:process_screenshot, true) do - screenshoter.take_screenshot(Pathname.new("tmp/01_a.png")) - end - end - - assert mock.verify + SnapDiff::BrowserHelpers.stub(:session, mock) do + screenshoter.stub(:process_screenshot, true) do + screenshoter.take_screenshot(Pathname.new("tmp/01_a.png")) end + end - test "#take_screenshot with custom screenshot options" do - screenshoter = SnapDiff::Screenshoter.new( - {wait: nil, capybara_screenshot_options: {full: true}}, - {driver: :chunky_png} - ) + assert mock.verify + end - mock = ::Minitest::Mock.new - mock.expect(:save_screenshot, true) { |path, options| path.include?("01_a.png") && options[:full] } + test "#take_screenshot with custom screenshot options" do + screenshoter = SnapDiff::Screenshoter.new( + {wait: nil, capybara_screenshot_options: {full: true}}, + {driver: :chunky_png} + ) - SnapDiff::BrowserHelpers.stub(:session, mock) do - screenshoter.stub(:process_screenshot, true) do - screenshoter.take_screenshot(Pathname.new("tmp/01_a.png")) - end - end + mock = ::Minitest::Mock.new + mock.expect(:save_screenshot, true) { |path, options| path.include?("01_a.png") && options[:full] } - assert mock.verify + SnapDiff::BrowserHelpers.stub(:session, mock) do + screenshoter.stub(:process_screenshot, true) do + screenshoter.take_screenshot(Pathname.new("tmp/01_a.png")) end + end - test "#prepare_page_for_screenshot without wait does not raise any error" do - screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png}) + assert mock.verify + end - assert_nil screenshoter.prepare_page_for_screenshot(timeout: nil) # does not raise an error - end + test "#prepare_page_for_screenshot without wait does not raise any error" do + screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png}) - test "#resize_if_needed halves a non-square retina screenshot to the expected window size via VipsDriver" do - skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - screenshoter = SnapDiff::Screenshoter.new({}, {driver: :vips}) - retina_image = Vips::Image.black(2560, 1600) # 2x window size, non-square + assert_nil screenshoter.prepare_page_for_screenshot(timeout: nil) # does not raise an error + end - resized = SnapDiff.config.stub(:window_size, [1280, 1024]) do - screenshoter.send(:resize_if_needed, retina_image) - end + test "#resize_if_needed halves a non-square retina screenshot to the expected window size via VipsDriver" do + skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) + screenshoter = SnapDiff::Screenshoter.new({}, {driver: :vips}) + retina_image = Vips::Image.black(2560, 1600) # 2x window size, non-square - assert_equal [1280, 800], screenshoter.driver.dimension(resized) - end + resized = SnapDiff.config.stub(:window_size, [1280, 1024]) do + screenshoter.send(:resize_if_needed, retina_image) end + + assert_equal [1280, 800], screenshoter.driver.dimension(resized) end end diff --git a/test/unit/snap_diff_config_test.rb b/test/unit/snap_diff_config_test.rb index 5663d26f..14bacd7e 100644 --- a/test/unit/snap_diff_config_test.rb +++ b/test/unit/snap_diff_config_test.rb @@ -76,59 +76,59 @@ def config end test "writing fail_if_new via the old mattr_accessor is visible via config, and back" do - original = Capybara::Screenshot::Diff.fail_if_new + original = SnapDiff.config.fail_if_new begin - Capybara::Screenshot::Diff.fail_if_new = true + SnapDiff.config.fail_if_new = true assert_equal true, config.fail_if_new config.fail_if_new = false - assert_equal false, Capybara::Screenshot::Diff.fail_if_new + assert_equal false, SnapDiff.config.fail_if_new ensure - Capybara::Screenshot::Diff.fail_if_new = original + SnapDiff.config.fail_if_new = original end end test "writing window_size via the old mattr_accessor is visible via config, and back" do - original = Capybara::Screenshot.window_size + original = SnapDiff.config.window_size begin - Capybara::Screenshot.window_size = [1280, 1024] + SnapDiff.config.window_size = [1280, 1024] assert_equal [1280, 1024], config.window_size config.window_size = [800, 600] - assert_equal [800, 600], Capybara::Screenshot.window_size + assert_equal [800, 600], SnapDiff.config.window_size ensure - Capybara::Screenshot.window_size = original + SnapDiff.config.window_size = original end end - # Capybara::Screenshot.enabled and Capybara::Screenshot::Diff.enabled are + # SnapDiff.config.screenshot_enabled and SnapDiff.config.enabled are # two independent settings that happen to share a bare name in their own - # modules (see Capybara::Screenshot.active?, which reads both). Config is + # modules (see SnapDiff.config.active?, which reads both). Config is # flat, so it cannot expose two attributes both called `enabled` -- the # Screenshot-side one is renamed `screenshot_enabled`. This test proves # the rename didn't accidentally collapse them into one shared value. test "screenshot_enabled and enabled stay independent settings under Config" do - original_screenshot = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.enabled + original_screenshot = SnapDiff.config.screenshot_enabled + original_diff = SnapDiff.config.enabled begin config.screenshot_enabled = true config.enabled = false - assert_equal true, Capybara::Screenshot.enabled - assert_equal false, Capybara::Screenshot::Diff.enabled + assert_equal true, SnapDiff.config.screenshot_enabled + assert_equal false, SnapDiff.config.enabled assert_equal true, config.screenshot_enabled assert_equal false, config.enabled ensure - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.enabled = original_diff + SnapDiff.config.screenshot_enabled = original_screenshot + SnapDiff.config.enabled = original_diff end end # ADR-008 step 7b moved this precedence rule from - # Capybara::Screenshot.active? into Config#active?, and found it had no + # SnapDiff.config.active? into Config#active?, and found it had no # test at all: replacing the whole expression with a bare `enabled` kept # all 529 unit tests green. The full truth table is pinned here, through # both the canonical method and the legacy forwarder, so it cannot move @@ -146,8 +146,8 @@ def config ].freeze test "active? gives Screenshot.enabled precedence and only falls through on nil" do - original_screenshot = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.enabled + original_screenshot = SnapDiff.config.screenshot_enabled + original_diff = SnapDiff.config.enabled ACTIVE_TRUTH_TABLE.each do |screenshot_enabled, enabled, expected| config.screenshot_enabled = screenshot_enabled @@ -155,23 +155,23 @@ def config context = "screenshot_enabled=#{screenshot_enabled.inspect}, enabled=#{enabled.inspect}" assert_equal expected, !!config.active?, "Config#active? with #{context}" - assert_equal expected, !!Capybara::Screenshot.active?, "Capybara::Screenshot.active? with #{context}" + assert_equal expected, !!SnapDiff.config.active?, "SnapDiff.config.active? with #{context}" end ensure - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.enabled = original_diff + SnapDiff.config.screenshot_enabled = original_screenshot + SnapDiff.config.enabled = original_diff end test "writing root through config round-trips through the same Pathname coercion" do - original = Capybara::Screenshot.root + original = SnapDiff.config.root begin config.root = "/tmp" - assert_equal Pathname("/tmp"), Capybara::Screenshot.root + assert_equal Pathname("/tmp"), SnapDiff.config.root assert_equal Pathname("/tmp"), config.root ensure - Capybara::Screenshot.root = original + SnapDiff.config.root = original end end @@ -183,13 +183,13 @@ def config end test "SnapDiff.configure lets callers set values through the yielded config" do - original = Capybara::Screenshot::Diff.tolerance + original = SnapDiff.config.tolerance begin SnapDiff.configure { |c| c.tolerance = 0.0321 } - assert_equal 0.0321, Capybara::Screenshot::Diff.tolerance + assert_equal 0.0321, SnapDiff.config.tolerance ensure - Capybara::Screenshot::Diff.tolerance = original + SnapDiff.config.tolerance = original end end diff --git a/test/unit/snap_diff_test.rb b/test/unit/snap_diff_test.rb index 5f3bcf65..ecacd753 100644 --- a/test/unit/snap_diff_test.rb +++ b/test/unit/snap_diff_test.rb @@ -4,18 +4,9 @@ require "open3" class SnapDiffTest < ActiveSupport::TestCase - # Deliberate legacy-name use: this test pins the public alias claim in its - # own name, so it silences the shim warning locally (the suite-wide guard - # in test_helper raises on unexpected ones). - test "SnapDiff::Comparison aliases Capybara::Screenshot::Diff::ImageCompare" do - original_silence = SnapDiff.silence_deprecations - SnapDiff.silence_deprecations = true - - assert_same Capybara::Screenshot::Diff::ImageCompare, SnapDiff::Comparison - ensure - SnapDiff.silence_deprecations = original_silence - end - + # The ImageCompare alias claim and the v1-shaped SnapDiff.start / .configure + # pair live in test/legacy/legacy_forwarders_test.rb -- both are v1 surface + # and go with it in 3.0. test ".compare returns the same kind of result as Diff.compare, forwarding options" do result = SnapDiff.compare( TEST_IMAGES_DIR / "a.png", @@ -48,7 +39,7 @@ class SnapDiffTest < ActiveSupport::TestCase # Regression test (#218 adversarial review): the probe above only builds a # comparison; annotation runs when a difference is actually reported, and # under bare `require "snap_diff"` that used to raise - # `NameError: uninitialized constant CapybaraScreenshotDiff::RED_RGBA` -- + # `NameError: uninitialized constant SnapDiff::RED_RGBA` -- # the annotation colors were defined only in the umbrella # capybara_screenshot_diff.rb, which this entry never loads. test "bare require \"snap_diff\" can annotate a difference between differing images" do @@ -114,26 +105,4 @@ class SnapDiffTest < ActiveSupport::TestCase SnapDiff.assert_single_gem!({"snap_diff-capybara" => :spec}) SnapDiff.assert_single_gem!({}) # local dev from source: neither spec loaded end - - test ".start yields the same objects Diff.configure yields" do - yielded = [] - Capybara::Screenshot::Diff.configure { |screenshot, diff| yielded << [screenshot, diff] } - - started = [] - SnapDiff.start { |screenshot, diff| started << [screenshot, diff] } - - assert_equal yielded, started - end - - test ".start applies a setting like Diff.configure does" do - original = Capybara::Screenshot::Diff.tolerance - - begin - SnapDiff.start { |_screenshot, diff| diff.tolerance = 0.0123 } - - assert_equal 0.0123, Capybara::Screenshot::Diff.tolerance - ensure - Capybara::Screenshot::Diff.tolerance = original - end - end end diff --git a/test/unit/snap_manager_cleanup_test.rb b/test/unit/snap_manager_cleanup_test.rb index c38828ab..fc191223 100644 --- a/test/unit/snap_manager_cleanup_test.rb +++ b/test/unit/snap_manager_cleanup_test.rb @@ -2,81 +2,79 @@ require "test_helper" -module CapybaraScreenshotDiff - # Guard #3 from the v2 core-redesign acceptance contract (D7). - # - # Documents the previously-dead class-method cleanup path: - # `SnapDiff::SnapManager.instance` used to build a brand-new manager on every call, so - # `SnapDiff::SnapManager.snapshot` tracked snapshots on one throwaway instance and - # `SnapDiff::SnapManager.cleanup!` iterated the empty set of another — test_helper's - # teardown cleanup had never deleted anything through the tracked path. - # These tests were RED before SnapDiff::SnapManager.instance was memoized per thread. - class SnapManagerCleanupTest < ActiveSupport::TestCase - test ".instance returns the same manager within a thread" do - assert_same SnapDiff::SnapManager.instance, SnapDiff::SnapManager.instance - end +# Guard #3 from the v2 core-redesign acceptance contract (D7). +# +# Documents the previously-dead class-method cleanup path: +# `SnapDiff::SnapManager.instance` used to build a brand-new manager on every call, so +# `SnapDiff::SnapManager.snapshot` tracked snapshots on one throwaway instance and +# `SnapDiff::SnapManager.cleanup!` iterated the empty set of another — test_helper's +# teardown cleanup had never deleted anything through the tracked path. +# These tests were RED before SnapDiff::SnapManager.instance was memoized per thread. +class SnapManagerCleanupTest < ActiveSupport::TestCase + test ".instance returns the same manager within a thread" do + assert_same SnapDiff::SnapManager.instance, SnapDiff::SnapManager.instance + end - test ".instance is rebuilt when the screenshot root changes" do - original = SnapDiff::SnapManager.instance + test ".instance is rebuilt when the screenshot root changes" do + original = SnapDiff::SnapManager.instance - Dir.mktmpdir do |dir| - Capybara::Screenshot.root = dir + Dir.mktmpdir do |dir| + SnapDiff.config.root = dir - rebuilt = SnapDiff::SnapManager.instance + rebuilt = SnapDiff::SnapManager.instance - assert_not_same original, rebuilt - assert_equal Pathname.new(Capybara::Screenshot.screenshot_area_abs), rebuilt.root - end + assert_not_same original, rebuilt + assert_equal Pathname.new(SnapDiff.config.screenshot_area_abs), rebuilt.root end + end - test ".cleanup! deletes files of snapshots tracked via the class-method path" do - snap = SnapDiff::SnapManager.snapshot("cleanup_guard") - provision(snap) + test ".cleanup! deletes files of snapshots tracked via the class-method path" do + snap = SnapDiff::SnapManager.snapshot("cleanup_guard") + provision(snap) - assert_predicate snap.path, :exist? - assert_predicate snap.base_path, :exist? + assert_predicate snap.path, :exist? + assert_predicate snap.base_path, :exist? - SnapDiff::SnapManager.cleanup! + SnapDiff::SnapManager.cleanup! - assert_not snap.path.exist?, "cleanup! must delete the actual screenshot tracked by SnapDiff::SnapManager.snapshot" - assert_not snap.base_path.exist?, "cleanup! must delete the base screenshot tracked by SnapDiff::SnapManager.snapshot" - end + assert_not snap.path.exist?, "cleanup! must delete the actual screenshot tracked by SnapDiff::SnapManager.snapshot" + assert_not snap.base_path.exist?, "cleanup! must delete the base screenshot tracked by SnapDiff::SnapManager.snapshot" + end + + test ".cleanup! in one thread does not delete snapshots tracked by another thread" do + barrier = Queue.new - test ".cleanup! in one thread does not delete snapshots tracked by another thread" do - barrier = Queue.new - - thread_b_snap = nil - thread_b = Thread.new do - thread_b_snap = SnapDiff::SnapManager.snapshot("cleanup_guard_thread_b") - provision(thread_b_snap) - barrier.pop # wait until thread A has cleaned up - end - - thread_a = Thread.new do - snap = SnapDiff::SnapManager.snapshot("cleanup_guard_thread_a") - provision(snap) - Thread.pass until thread_b_snap&.path&.exist? - - SnapDiff::SnapManager.cleanup! - snap - end - - thread_a_snap = thread_a.value - barrier << :done - thread_b.join - - assert_not thread_a_snap.path.exist?, "thread A's own snapshot should be cleaned up" - assert_predicate thread_b_snap.path, :exist?, "thread B's snapshot must survive thread A's cleanup!" - ensure - thread_b_snap&.delete! + thread_b_snap = nil + thread_b = Thread.new do + thread_b_snap = SnapDiff::SnapManager.snapshot("cleanup_guard_thread_b") + provision(thread_b_snap) + barrier.pop # wait until thread A has cleaned up end - private + thread_a = Thread.new do + snap = SnapDiff::SnapManager.snapshot("cleanup_guard_thread_a") + provision(snap) + Thread.pass until thread_b_snap&.path&.exist? - def provision(snap) - snap.path.dirname.mkpath - FileUtils.cp(fixture_image_path_from("a"), snap.path) - FileUtils.cp(fixture_image_path_from("a"), snap.base_path) + SnapDiff::SnapManager.cleanup! + snap end + + thread_a_snap = thread_a.value + barrier << :done + thread_b.join + + assert_not thread_a_snap.path.exist?, "thread A's own snapshot should be cleaned up" + assert_predicate thread_b_snap.path, :exist?, "thread B's snapshot must survive thread A's cleanup!" + ensure + thread_b_snap&.delete! + end + + private + + def provision(snap) + snap.path.dirname.mkpath + FileUtils.cp(fixture_image_path_from("a"), snap.path) + FileUtils.cp(fixture_image_path_from("a"), snap.base_path) end end diff --git a/test/unit/snap_manager_test.rb b/test/unit/snap_manager_test.rb index 2e17dcad..35ce3c8e 100644 --- a/test/unit/snap_manager_test.rb +++ b/test/unit/snap_manager_test.rb @@ -2,123 +2,121 @@ require "test_helper" -module CapybaraScreenshotDiff - class SnapManagerTest < ActiveSupport::TestCase - setup do - @manager = SnapDiff::SnapManager.new(Dir.mktmpdir("snap_diff-storage")) - end +class SnapManagerTest < ActiveSupport::TestCase + setup do + @manager = SnapDiff::SnapManager.new(Dir.mktmpdir("snap_diff-storage")) + end - teardown do - @manager.cleanup! - end + teardown do + @manager.cleanup! + end - test "#provision_snap_with copies the file to the snap path" do - snap = @manager.snapshot("test_image") - path = fixture_image_path_from("a") + test "#provision_snap_with copies the file to the snap path" do + snap = @manager.snapshot("test_image") + path = fixture_image_path_from("a") - @manager.provision_snap_with(snap, path) + @manager.provision_snap_with(snap, path) - assert_predicate snap.path, :exist? - assert_not_predicate snap.base_path, :exist? - end + assert_predicate snap.path, :exist? + assert_not_predicate snap.base_path, :exist? + end - test "#provision_snap_with populate the base version of the snapshot" do - snap = @manager.snapshot("test_image") - path = fixture_image_path_from("a") + test "#provision_snap_with populate the base version of the snapshot" do + snap = @manager.snapshot("test_image") + path = fixture_image_path_from("a") - @manager.provision_snap_with(snap, path, version: :base) + @manager.provision_snap_with(snap, path, version: :base) - assert_not_predicate snap.path, :exist? - assert_predicate snap.base_path, :exist? - end + assert_not_predicate snap.path, :exist? + assert_predicate snap.base_path, :exist? + end - test "#screenshots_dir returns all created snapshots" do - assert_equal [], @manager.snapshots.to_a + test "#screenshots_dir returns all created snapshots" do + assert_equal [], @manager.snapshots.to_a - snap = @manager.snapshot("test_image") - path = fixture_image_path_from("a") + snap = @manager.snapshot("test_image") + path = fixture_image_path_from("a") - @manager.provision_snap_with(snap, path) - assert_equal [snap], @manager.snapshots.to_a - end + @manager.provision_snap_with(snap, path) + assert_equal [snap], @manager.snapshots.to_a + end - test "#screenshots_dir ignores attempts" do - assert_equal [], @manager.snapshots.to_a + test "#screenshots_dir ignores attempts" do + assert_equal [], @manager.snapshots.to_a - snap = @manager.snapshot("test_image") - path = fixture_image_path_from("a") + snap = @manager.snapshot("test_image") + path = fixture_image_path_from("a") - @manager.provision_snap_with(snap, path, version: :attempt) + @manager.provision_snap_with(snap, path, version: :attempt) - assert_equal [snap], @manager.snapshots.to_a - end + assert_equal [snap], @manager.snapshots.to_a + end - test "#snapshot overrides the file extension" do - snap = @manager.snapshot("test_image") - assert_equal "test_image", snap.full_name - assert_includes snap.path.to_s, "test_image.png" - assert_includes snap.base_path.to_s, "test_image.base.png" - assert_includes snap.next_attempt_path!.to_s, "test_image.attempt_00.png" - end + test "#snapshot overrides the file extension" do + snap = @manager.snapshot("test_image") + assert_equal "test_image", snap.full_name + assert_includes snap.path.to_s, "test_image.png" + assert_includes snap.base_path.to_s, "test_image.base.png" + assert_includes snap.next_attempt_path!.to_s, "test_image.attempt_00.png" + end - # With a per-thread long-lived manager, cleanup! must also empty the - # tracked set — otherwise it grows for the whole run and a later cleanup! - # can delete a path an earlier test tracked but this test re-provisioned. - test "#cleanup! empties the tracked set" do - snap = @manager.snapshot("tracked_then_cleaned") - @manager.provision_snap_with(snap, fixture_image_path_from("a")) + # With a per-thread long-lived manager, cleanup! must also empty the + # tracked set — otherwise it grows for the whole run and a later cleanup! + # can delete a path an earlier test tracked but this test re-provisioned. + test "#cleanup! empties the tracked set" do + snap = @manager.snapshot("tracked_then_cleaned") + @manager.provision_snap_with(snap, fixture_image_path_from("a")) - @manager.cleanup! + @manager.cleanup! - assert_empty @manager.snapshots - end + assert_empty @manager.snapshots + end - # Store-API split guard (v2 amendment item 1): #path_for is the pure - # lookup half of what #snapshot used to be used for. Callers that only - # need paths (e.g. dsl_test's post-capture assertions) must be able to - # look up without registering the name for cleanup!. - test "#path_for resolves the same paths as #snapshot" do - registered = @manager.snapshot("split_guard") - looked_up = @manager.path_for("split_guard") + # Store-API split guard (v2 amendment item 1): #path_for is the pure + # lookup half of what #snapshot used to be used for. Callers that only + # need paths (e.g. dsl_test's post-capture assertions) must be able to + # look up without registering the name for cleanup!. + test "#path_for resolves the same paths as #snapshot" do + registered = @manager.snapshot("split_guard") + looked_up = @manager.path_for("split_guard") - assert_equal registered.path, looked_up.path - assert_equal registered.base_path, looked_up.base_path - end + assert_equal registered.path, looked_up.path + assert_equal registered.base_path, looked_up.base_path + end - test "#path_for never registers the snapshot for cleanup" do - @manager.path_for("split_guard") + test "#path_for never registers the snapshot for cleanup" do + @manager.path_for("split_guard") - assert_empty @manager.snapshots.to_a - end + assert_empty @manager.snapshots.to_a + end - test ".path_for is a pure lookup on the shared instance" do - tracked_before = SnapDiff::SnapManager.instance.snapshots.dup + test ".path_for is a pure lookup on the shared instance" do + tracked_before = SnapDiff::SnapManager.instance.snapshots.dup - looked_up = SnapDiff::SnapManager.path_for("split_guard_class") + looked_up = SnapDiff::SnapManager.path_for("split_guard_class") - assert_equal tracked_before, SnapDiff::SnapManager.instance.snapshots + assert_equal tracked_before, SnapDiff::SnapManager.instance.snapshots - registered = SnapDiff::SnapManager.snapshot("split_guard_class") - assert_includes SnapDiff::SnapManager.instance.snapshots, registered - assert_equal registered.path, looked_up.path - end + registered = SnapDiff::SnapManager.snapshot("split_guard_class") + assert_includes SnapDiff::SnapManager.instance.snapshots, registered + assert_equal registered.path, looked_up.path + end - test "#cleanup! removes diff artifacts created by reporters" do - snap = @manager.snapshot("test_image") - source = fixture_image_path_from("a") - @manager.provision_snap_with(snap, source) - @manager.provision_snap_with(snap, source, version: :base) + test "#cleanup! removes diff artifacts created by reporters" do + snap = @manager.snapshot("test_image") + source = fixture_image_path_from("a") + @manager.provision_snap_with(snap, source) + @manager.provision_snap_with(snap, source, version: :base) - diff_path = snap.path.sub_ext(".diff.png") - base_diff_path = snap.path.sub_ext(".base.diff.png") - heatmap_path = snap.path.sub_ext(".heatmap.diff.png") - [diff_path, base_diff_path, heatmap_path].each { |p| FileUtils.cp(source, p) } + diff_path = snap.path.sub_ext(".diff.png") + base_diff_path = snap.path.sub_ext(".base.diff.png") + heatmap_path = snap.path.sub_ext(".heatmap.diff.png") + [diff_path, base_diff_path, heatmap_path].each { |p| FileUtils.cp(source, p) } - @manager.cleanup! + @manager.cleanup! - assert_not diff_path.exist?, "diff artifact should be cleaned up" - assert_not base_diff_path.exist?, "base diff artifact should be cleaned up" - assert_not heatmap_path.exist?, "heatmap diff artifact should be cleaned up" - end + assert_not diff_path.exist?, "diff artifact should be cleaned up" + assert_not base_diff_path.exist?, "base diff artifact should be cleaned up" + assert_not heatmap_path.exist?, "heatmap diff artifact should be cleaned up" end end diff --git a/test/unit/stable_screenshoter_test.rb b/test/unit/stable_screenshoter_test.rb index 4291d595..3a73d3b3 100644 --- a/test/unit/stable_screenshoter_test.rb +++ b/test/unit/stable_screenshoter_test.rb @@ -2,122 +2,116 @@ require "test_helper" -module Capybara - module Screenshot - module Diff - class StableScreenshoterTest < ActiveSupport::TestCase - include CapybaraScreenshotDiff::DSLStub - - def setup - super - @manager = SnapDiff::SnapManager.new(Capybara::Screenshot.root / "stable_screenshoter_test") - @manager.create_output_directory_for - end - - def teardown - @manager.cleanup! - super - end - - test "#take_stable_screenshot retries until images are stable across iterations" do - image_compare_stub = build_image_compare_stub - - mock = ::Minitest::Mock.new(image_compare_stub) - - mock.expect(:quick_equal?, false) - mock.expect(:quick_equal?, false) - mock.expect(:quick_equal?, true) - - SnapDiff::Comparison.stub :new, mock do - snap = @manager.snapshot("02_a") - take_stable_screenshot_with(snap) - end - - assert mock.verify - end - - test "#take_stable_screenshot raises ArgumentError when wait parameter is nil" do - assert_raises ArgumentError, "wait should be provided" do - take_stable_screenshot_with(@manager.snapshot("02_a"), wait: nil) - end - end - - test "#take_stable_screenshot raises ArgumentError when stability_time_limit is nil" do - assert_raises ArgumentError, "stability_time_limit should be provided" do - take_stable_screenshot_with(@manager.snapshot("02_a"), stability_time_limit: nil) - end - end - - test "#take_comparison_screenshot cleans up temporary files after successful comparison" do - image_compare_stub = build_image_compare_stub - - mock = ::Minitest::Mock.new(image_compare_stub) - mock.expect(:quick_equal?, false) - mock.expect(:quick_equal?, true) - - snap = @manager.snapshot("02_a") - assert_not_predicate snap.path, :exist? - - SnapDiff::Comparison.stub :new, mock do - SnapDiff::StableScreenshoter - .new({stability_time_limit: 0.5, wait: 1}, image_compare_stub.driver_options) - .take_comparison_screenshot(snap) - end - - mock.verify - assert_empty snap.find_attempts_paths - assert_predicate snap.path, :exist? - assert_not_predicate snap.path.size, :zero? - end - - test "#take_comparison_screenshot raises UnstableImage when stability timeout is reached" do - snap = @manager.snapshot("01_a") - - screenshot_path = snap.path - - # Stub annotated files for generated comparison annotations - # We need to have different from screenshot_path name because of other stubs - pseudo_snap_for_annotations = @manager.snapshot("02_a") - annotated_screenshot_path = pseudo_snap_for_annotations.path - annotated_attempts_paths = [ - [annotated_screenshot_path.sub_ext(".attempt_01.latest.png"), annotated_screenshot_path.sub_ext(".attempt_01.committed.png")], - [annotated_screenshot_path.sub_ext(".attempt_02.latest.png"), annotated_screenshot_path.sub_ext(".attempt_02.committed.png")] - ] - - FileUtils.touch(annotated_attempts_paths) - - mock = ::Minitest::Mock.new(build_image_compare_stub(equal: false)) - annotated_attempts_paths.reverse_each do |(actual_path, base_path)| - mock.reporter.expect(:annotated_image_path, actual_path.to_s) - mock.reporter.expect(:annotated_base_image_path, base_path.to_s) - end - - assert_raises CapybaraScreenshotDiff::UnstableImage, "Could not get stable screenshot within 1s" do - SnapDiff::Comparison.stub :new, mock do - # Wait time is less then stability time, which will generate problem - SnapDiff::StableScreenshoter - .new({stability_time_limit: 0.5, wait: 1}, build_image_compare_stub(equal: false).driver_options) - .take_comparison_screenshot(snap) - end - end - - mock.verify - mock.reporter.verify - - # There are no runtime files to find difference on stabilization - assert_empty Dir["tmp/*_a*.latest.png"] - assert_empty Dir["tmp/*_a*.committed.png"] - - # All stabilization files should be annotated - last_annotation = screenshot_path.sub_ext(".attempt_02.png") - assert_equal 0, last_annotation.size, "#{last_annotation.to_path} should be override with annotated version" - last_annotation = screenshot_path.sub_ext(".attempt_01.png") - assert_equal 0, last_annotation.size, "#{last_annotation.to_path} should be override with annotated version" - ensure - snap&.delete! - pseudo_snap_for_annotations&.delete! - end +class StableScreenshoterTest < ActiveSupport::TestCase + include DSLStub + + def setup + super + @manager = SnapDiff::SnapManager.new(SnapDiff.config.root / "stable_screenshoter_test") + @manager.create_output_directory_for + end + + def teardown + @manager.cleanup! + super + end + + test "#take_stable_screenshot retries until images are stable across iterations" do + image_compare_stub = build_image_compare_stub + + mock = ::Minitest::Mock.new(image_compare_stub) + + mock.expect(:quick_equal?, false) + mock.expect(:quick_equal?, false) + mock.expect(:quick_equal?, true) + + SnapDiff::Comparison.stub :new, mock do + snap = @manager.snapshot("02_a") + take_stable_screenshot_with(snap) + end + + assert mock.verify + end + + test "#take_stable_screenshot raises ArgumentError when wait parameter is nil" do + assert_raises ArgumentError, "wait should be provided" do + take_stable_screenshot_with(@manager.snapshot("02_a"), wait: nil) + end + end + + test "#take_stable_screenshot raises ArgumentError when stability_time_limit is nil" do + assert_raises ArgumentError, "stability_time_limit should be provided" do + take_stable_screenshot_with(@manager.snapshot("02_a"), stability_time_limit: nil) + end + end + + test "#take_comparison_screenshot cleans up temporary files after successful comparison" do + image_compare_stub = build_image_compare_stub + + mock = ::Minitest::Mock.new(image_compare_stub) + mock.expect(:quick_equal?, false) + mock.expect(:quick_equal?, true) + + snap = @manager.snapshot("02_a") + assert_not_predicate snap.path, :exist? + + SnapDiff::Comparison.stub :new, mock do + SnapDiff::StableScreenshoter + .new({stability_time_limit: 0.5, wait: 1}, image_compare_stub.driver_options) + .take_comparison_screenshot(snap) + end + + mock.verify + assert_empty snap.find_attempts_paths + assert_predicate snap.path, :exist? + assert_not_predicate snap.path.size, :zero? + end + + test "#take_comparison_screenshot raises UnstableImage when stability timeout is reached" do + snap = @manager.snapshot("01_a") + + screenshot_path = snap.path + + # Stub annotated files for generated comparison annotations + # We need to have different from screenshot_path name because of other stubs + pseudo_snap_for_annotations = @manager.snapshot("02_a") + annotated_screenshot_path = pseudo_snap_for_annotations.path + annotated_attempts_paths = [ + [annotated_screenshot_path.sub_ext(".attempt_01.latest.png"), annotated_screenshot_path.sub_ext(".attempt_01.committed.png")], + [annotated_screenshot_path.sub_ext(".attempt_02.latest.png"), annotated_screenshot_path.sub_ext(".attempt_02.committed.png")] + ] + + FileUtils.touch(annotated_attempts_paths) + + mock = ::Minitest::Mock.new(build_image_compare_stub(equal: false)) + annotated_attempts_paths.reverse_each do |(actual_path, base_path)| + mock.reporter.expect(:annotated_image_path, actual_path.to_s) + mock.reporter.expect(:annotated_base_image_path, base_path.to_s) + end + + assert_raises SnapDiff::UnstableImage, "Could not get stable screenshot within 1s" do + SnapDiff::Comparison.stub :new, mock do + # Wait time is less then stability time, which will generate problem + SnapDiff::StableScreenshoter + .new({stability_time_limit: 0.5, wait: 1}, build_image_compare_stub(equal: false).driver_options) + .take_comparison_screenshot(snap) end end + + mock.verify + mock.reporter.verify + + # There are no runtime files to find difference on stabilization + assert_empty Dir["tmp/*_a*.latest.png"] + assert_empty Dir["tmp/*_a*.committed.png"] + + # All stabilization files should be annotated + last_annotation = screenshot_path.sub_ext(".attempt_02.png") + assert_equal 0, last_annotation.size, "#{last_annotation.to_path} should be override with annotated version" + last_annotation = screenshot_path.sub_ext(".attempt_01.png") + assert_equal 0, last_annotation.size, "#{last_annotation.to_path} should be override with annotated version" + ensure + snap&.delete! + pseudo_snap_for_annotations&.delete! end end diff --git a/test/unit/static_test.rb b/test/unit/static_test.rb index 08e75253..7dbab817 100644 --- a/test/unit/static_test.rb +++ b/test/unit/static_test.rb @@ -1,36 +1,35 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff/static" +require "snap_diff/static" -module CapybaraScreenshotDiff - class StaticTest < ActiveSupport::TestCase - setup do - @original_root = Capybara::Screenshot.root - end +class StaticTest < ActiveSupport::TestCase + setup do + @original_root = SnapDiff.config.root + end - teardown do - Capybara.app = Rails.application - Capybara::Screenshot.root = @original_root - end + teardown do + Capybara.app = Rails.application + SnapDiff.config.root = @original_root + end - test ".serve sets Capybara.app to serve the directory" do - SnapDiff.serve("test/fixtures") + test ".serve sets Capybara.app to serve the directory" do + SnapDiff.serve("test/fixtures") - assert_kind_of Rack::Files, Capybara.app - end + assert_kind_of Rack::Files, Capybara.app + end - test ".serve sets Screenshot.root to pwd" do - SnapDiff.serve("test/fixtures") + test ".serve sets Screenshot.root to pwd" do + SnapDiff.serve("test/fixtures") - assert_equal Pathname(Dir.pwd), Capybara::Screenshot.root - end + assert_equal Pathname(Dir.pwd), SnapDiff.config.root + end - test ".serve accepts custom root (via the legacy CapybaraScreenshotDiff.serve forwarder)" do - # Deliberate legacy-surface use: pins that the old entry still forwards. - CapybaraScreenshotDiff.serve("test/fixtures", root: "/tmp") + # The legacy CapybaraScreenshotDiff.serve forwarder over this is pinned in + # test/legacy/legacy_forwarders_test.rb. + test ".serve accepts custom root" do + SnapDiff.serve("test/fixtures", root: "/tmp") - assert_equal Pathname("/tmp"), Capybara::Screenshot.root - end + assert_equal Pathname("/tmp"), SnapDiff.config.root end end diff --git a/test/unit/support_load_probe_test.rb b/test/unit/support_load_probe_test.rb index 48e83d99..53d71485 100644 --- a/test/unit/support_load_probe_test.rb +++ b/test/unit/support_load_probe_test.rb @@ -10,7 +10,7 @@ # own requires still loads fine in most suite runs and only breaks in the CI # matrix cell with a different load order — exactly how # setup_capybara_drivers.rb broke on selenium_chrome_headless + vips (it used -# Capybara::Screenshot::Os without requiring it, fixed in b7ada5e). This test +# SnapDiff::Os without requiring it, fixed in b7ada5e). This test # requires every test/support file in a bare subprocess with only capybara # core preloaded. Scope: it catches missing requires for constants referenced # AT LOAD TIME under this process's env; references hidden behind env guards @@ -53,7 +53,7 @@ class SupportLoadProbeTest < ActiveSupport::TestCase # Alias-completeness probe (the f89cea2 bug class): each documented entry # point must define its advertised constants when it is the ONLY require — # the acyclic redesign once narrowed capybara_screenshot_diff/minitest so - # consumers lost CapybaraScreenshotDiff::DSL, and only one CI matrix leg + # consumers lost SnapDiff::DSL, and only one CI matrix leg # noticed. capybara_screenshot_diff/cucumber is not probed: it calls # World(...) at load, which only exists inside cucumber's runtime context. # Documented user-facing constants that must stay EAGER (see @@ -61,8 +61,8 @@ class SupportLoadProbeTest < ActiveSupport::TestCase # triggers const_missing, so a lazy shim makes `defined?` feature # detection in adopter code silently return nil. EAGER_USER_FACING = %w[ - Capybara::Screenshot::Diff::Reporters::Default - Capybara::Screenshot::Diff::Comparison + SnapDiff::Reporters::Default + SnapDiff::Comparison::Images ].freeze # The subset of EAGER_USER_FACING that must resolve under EVERY entry @@ -74,24 +74,24 @@ class SupportLoadProbeTest < ActiveSupport::TestCase # loaded the forwarder that assigned it, and const_missing does not fire # for a constant legacy_shims deliberately leaves out of its map. EAGER_EVERYWHERE = %w[ - Capybara::Screenshot::Diff::VERSION - Capybara::Screenshot::Diff::Comparison + SnapDiff::VERSION + SnapDiff::Comparison::Images ].freeze ENTRY_POINTS = { "capybara_screenshot_diff" => %w[ - CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff + SnapDiff::DSL SnapDiff::Os Capybara::Screenshot::Diff ] + EAGER_USER_FACING, "capybara_screenshot_diff/minitest" => %w[ - CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions - Capybara::Screenshot::Os Capybara::Screenshot::Diff + SnapDiff::DSL SnapDiff::Minitest::Assertions + SnapDiff::Os Capybara::Screenshot::Diff ] + EAGER_USER_FACING, "capybara_screenshot_diff/rspec" => %w[ - CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff + SnapDiff::DSL SnapDiff::Os Capybara::Screenshot::Diff ] + EAGER_USER_FACING, "capybara-screenshot-diff" => %w[ - CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions - Capybara::Screenshot::Os Capybara::Screenshot::Diff + SnapDiff::DSL SnapDiff::Minitest::Assertions + SnapDiff::Os Capybara::Screenshot::Diff ] + EAGER_USER_FACING }.freeze @@ -118,7 +118,7 @@ class SupportLoadProbeTest < ActiveSupport::TestCase # left SnapDiff.configure/.start/.compare undefined, SnapDiff::VERSION # unresolvable, and the dual-install guard silent. The legacy entries had # the mirror-image hole: some of them stopped loading the umbrella, so - # CapybaraScreenshotDiff.verify and friends vanished while + # SnapDiff.session.verify and friends vanished while # `defined?(CapybaraScreenshotDiff)` still passed. # SnapDiff.serve is deliberately absent here: docs/snapdiff.md's object diff --git a/test/unit/vcs_test.rb b/test/unit/vcs_test.rb index dde17d28..4efb3efc 100644 --- a/test/unit/vcs_test.rb +++ b/test/unit/vcs_test.rb @@ -2,37 +2,31 @@ require "test_helper" -module Capybara - module Screenshot - module Diff - class VcsTest < ActiveSupport::TestCase - include SnapDiff::Vcs - - PROJECT_ROOT = Pathname.new(File.expand_path("../..", __dir__)) - - setup do - @tmp_dir = PROJECT_ROOT / "tmp" / "vcs_test_#{Process.pid}" - FileUtils.mkdir_p(@tmp_dir) - @base_screenshot = Tempfile.new(%w[vcs_base. .png], @tmp_dir.to_s) - end - - teardown do - @base_screenshot&.close - @base_screenshot&.unlink - FileUtils.rm_rf(@tmp_dir) - end - - test "#checkout_vcs checks out and verifies the original screenshot" do - screenshot_path = file_fixture("images/a.png") - base_screenshot_path = Pathname.new(@base_screenshot.path) - - assert SnapDiff::Vcs.checkout_vcs(@tmp_dir, screenshot_path, base_screenshot_path), - "checkout_vcs failed: root=#{@tmp_dir}" - - assert base_screenshot_path.exist? - assert_equal screenshot_path.size, base_screenshot_path.size - end - end - end +class VcsTest < ActiveSupport::TestCase + include SnapDiff::Vcs + + PROJECT_ROOT = Pathname.new(File.expand_path("../..", __dir__)) + + setup do + @tmp_dir = PROJECT_ROOT / "tmp" / "vcs_test_#{Process.pid}" + FileUtils.mkdir_p(@tmp_dir) + @base_screenshot = Tempfile.new(%w[vcs_base. .png], @tmp_dir.to_s) + end + + teardown do + @base_screenshot&.close + @base_screenshot&.unlink + FileUtils.rm_rf(@tmp_dir) + end + + test "#checkout_vcs checks out and verifies the original screenshot" do + screenshot_path = file_fixture("images/a.png") + base_screenshot_path = Pathname.new(@base_screenshot.path) + + assert SnapDiff::Vcs.checkout_vcs(@tmp_dir, screenshot_path, base_screenshot_path), + "checkout_vcs failed: root=#{@tmp_dir}" + + assert base_screenshot_path.exist? + assert_equal screenshot_path.size, base_screenshot_path.size end end From e789c8a65c4de034b9e17f880a4068d96dc161c4 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:00:41 +0200 Subject: [PATCH 3/5] test: split the mixed config/entry-point files along the same line Four files asserted the canonical behaviour AND the v1 view of it in one place, so a receiver repoint turned real claims into tautologies. Each is now two files; the v1 half is verbatim, and the canonical half stands on its own after 3.0: - snap_diff_config_test -> + test/legacy/legacy_config_accessors_test (CONFIG_MAPPING completeness, the mattr_accessor round trips, active? through the legacy forwarder, SnapDiff.start) - config_default_timing_test -> + test/legacy/legacy_config_default_timing_test canonical keeps snap_diff + snap_diff/integrations/minitest and reads SnapDiff.config only; the legacy file re-runs the SAME probe scripts under the v1 entries and adds the both-surfaces-agree loop, which is exactly what check_both asserted -- one source of truth, no drift - support_load_probe_test -> + test/legacy/legacy_entry_point_probe_test (advertised v1 constants, the CapybaraScreenshotDiff session surface, EAGER_USER_FACING / EAGER_EVERYWHERE under their OLD names) - errors_alias_test (earlier commit) -> test/unit/errors_test snap_diff-capybara joins CANONICAL_ENTRY_POINTS: 3.0 keeps that entry point (repointed at snap_diff/integrations/minitest), and it was covered only as a legacy entry, so it would have lost all coverage. Also repointed the last legacy call sites the sweep left: the rspec fixtures stubbed Capybara::Screenshot::Diff.pending_if_new, which the core stopped reading in #235 -- a silent no-op stub, now SnapDiff.config. rake test:unit 544 runs, 1544 assertions, 0F/0E rake test 572 runs, 1589 assertions, 0F/0E/1S --- .../rspec_after_hook_order_masking_spec.rb | 2 +- test/fixtures/rspec_pending_masking_spec.rb | 2 +- test/fixtures/rspec_spec.rb | 4 +- test/legacy/legacy_config_accessors_test.rb | 196 ++++++++++++++++++ .../legacy_config_default_timing_test.rb | 58 ++++++ test/legacy/legacy_entry_point_probe_test.rb | 141 +++++++++++++ test/unit/config_default_timing_test.rb | 111 +++++----- test/unit/diff_test.rb | 6 +- test/unit/drivers/chunky_png_driver_test.rb | 2 +- test/unit/drivers/vips_driver_test.rb | 2 +- test/unit/dsl_test.rb | 4 +- test/unit/image_compare_test.rb | 2 +- test/unit/reporters_mutex_test.rb | 2 +- test/unit/snap_diff_config_test.rb | 164 ++++----------- test/unit/support_load_probe_test.rb | 155 ++------------ 15 files changed, 516 insertions(+), 335 deletions(-) create mode 100644 test/legacy/legacy_config_accessors_test.rb create mode 100644 test/legacy/legacy_config_default_timing_test.rb create mode 100644 test/legacy/legacy_entry_point_probe_test.rb diff --git a/test/fixtures/rspec_after_hook_order_masking_spec.rb b/test/fixtures/rspec_after_hook_order_masking_spec.rb index 1713a905..674acd05 100644 --- a/test/fixtures/rspec_after_hook_order_masking_spec.rb +++ b/test/fixtures/rspec_after_hook_order_masking_spec.rb @@ -56,7 +56,7 @@ it "keeps a real after-hook failure failing even when a new screenshot is pending" do name = "pending-masking-after-hook-order" - allow(Capybara::Screenshot::Diff).to receive(:pending_if_new).and_return(true) + allow(SnapDiff.config).to receive(:pending_if_new).and_return(true) visit "/" screenshot name ensure diff --git a/test/fixtures/rspec_pending_masking_spec.rb b/test/fixtures/rspec_pending_masking_spec.rb index 766e8de0..0848639e 100644 --- a/test/fixtures/rspec_pending_masking_spec.rb +++ b/test/fixtures/rspec_pending_masking_spec.rb @@ -43,7 +43,7 @@ it "keeps a genuine failure failing even when a new screenshot is pending" do name = "pending-masking-real-failure" - allow(Capybara::Screenshot::Diff).to receive(:pending_if_new).and_return(true) + allow(SnapDiff.config).to receive(:pending_if_new).and_return(true) visit "/" screenshot name diff --git a/test/fixtures/rspec_spec.rb b/test/fixtures/rspec_spec.rb index 395f4c52..fbbc5347 100644 --- a/test/fixtures/rspec_spec.rb +++ b/test/fixtures/rspec_spec.rb @@ -24,7 +24,7 @@ SnapDiff.config.tolerance = 0.5 end - it "should include CapybaraScreenshotDiff in rspec" do + it "should include SnapDiff::DSL in rspec" do expect(self.class.ancestors).to include SnapDiff::DSL end @@ -45,7 +45,7 @@ it "marks the example pending when a new screenshot has no baseline and pending_if_new is enabled" do name = "pending-if-new-example" - allow(Capybara::Screenshot::Diff).to receive(:pending_if_new).and_return(true) + allow(SnapDiff.config).to receive(:pending_if_new).and_return(true) visit "/" screenshot name ensure diff --git a/test/legacy/legacy_config_accessors_test.rb b/test/legacy/legacy_config_accessors_test.rb new file mode 100644 index 00000000..f52f6f13 --- /dev/null +++ b/test/legacy/legacy_config_accessors_test.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require "test_helper" +# The shared harness loads canonical entry points only, so a legacy-surface +# test pulls in the v1 entry itself -- the require goes with the file in 3.0. +require "capybara_screenshot_diff" + +# LEGACY SURFACE (test/legacy/, see the Rakefile). +# +# The v1 half of snap_diff_config_test.rb: SnapDiff::LegacyShims generates +# the old Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessors +# as a second VIEW of the one SnapDiff::Config storage. Everything here is +# about that view -- the mapping's completeness, and that a write through +# either surface is visible from the other. Verbatim from the canonical +# file, which keeps the Config-only half; both go on passing until 3.0 +# deletes legacy_shims.rb, this file, and the trees they serve. +class LegacyConfigAccessorsTest < ActiveSupport::TestCase + def config + SnapDiff.config + end + + # Reflection-based completeness check, reworked for the ADR-008 storage + # inversion (the old version derived settings from mattr class variables, + # which no longer exist). Two directions: + # + # (a) every singleton writer on the legacy modules is a mapped config + # setting -- a future `mattr_accessor :foo` (active_support's ext is + # one require away) or hand-rolled writer would create unmapped + # storage invisible to SnapDiff.config; + # (b) SnapDiff.config stores exactly one ivar per declared setting (that + # half stays in the canonical file: Config outlives the mapping). + NON_CONFIG_WRITERS = [].freeze # currently no non-config writer API on the legacy modules + + test "every legacy singleton writer is covered by SnapDiff::LegacyShims::CONFIG_MAPPING" do + covered = SnapDiff::LegacyShims::CONFIG_MAPPING.values + + [Capybara::Screenshot, Capybara::Screenshot::Diff].each do |mod| + writers = mod.singleton_class.public_instance_methods(false).grep(/=\z/) - NON_CONFIG_WRITERS + + assert_operator writers.size, :>, 0, "#{mod} lost all its config writers" + writers.each do |writer| + mattr = writer.to_s.delete_suffix("=").to_sym + + assert_includes covered, [mod, mattr], + "#{mod}.#{writer} is a config writer with no SnapDiff::Config mapping. " \ + "Add an entry to LegacyShims::CONFIG_MAPPING (rename the key if `#{mattr}` collides " \ + "with an existing one, as `enabled` does), or add it to NON_CONFIG_WRITERS " \ + "if it is deliberately not a config setting." + end + end + end + + # The two halves of the split are only safe while they agree: Config + # declares the settings and knows nothing about the legacy holders, + # LegacyShims says which holder each one is exposed on. A setting in one + # and not the other is either storage with no v1 accessor or a v1 + # accessor delegating to a setting that does not exist. + test "LegacyShims::CONFIG_MAPPING covers exactly Config::SETTINGS" do + assert_equal SnapDiff::Config::SETTINGS, SnapDiff::LegacyShims::CONFIG_MAPPING.keys + end + + test "every mapped setting is readable via config and equal to its mattr_accessor's value" do + SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| + expected = mod.public_send(mattr) + actual = config.public_send(name) + + if expected.nil? + assert_nil actual, "config.#{name} should equal #{mod}.#{mattr}" + else + assert_equal expected, actual, "config.#{name} should equal #{mod}.#{mattr}" + end + end + end + + test "writing fail_if_new via the old mattr_accessor is visible via config, and back" do + original = Capybara::Screenshot::Diff.fail_if_new + + begin + Capybara::Screenshot::Diff.fail_if_new = true + assert_equal true, config.fail_if_new + + config.fail_if_new = false + assert_equal false, Capybara::Screenshot::Diff.fail_if_new + ensure + Capybara::Screenshot::Diff.fail_if_new = original + end + end + + test "writing window_size via the old mattr_accessor is visible via config, and back" do + original = Capybara::Screenshot.window_size + + begin + Capybara::Screenshot.window_size = [1280, 1024] + assert_equal [1280, 1024], config.window_size + + config.window_size = [800, 600] + assert_equal [800, 600], Capybara::Screenshot.window_size + ensure + Capybara::Screenshot.window_size = original + end + end + + # Capybara::Screenshot.enabled and Capybara::Screenshot::Diff.enabled are + # two independent settings that happen to share a bare name in their own + # modules (see Capybara::Screenshot.active?, which reads both). Config is + # flat, so it cannot expose two attributes both called `enabled` -- the + # Screenshot-side one is renamed `screenshot_enabled`. This test proves + # the rename didn't accidentally collapse them into one shared value. + test "screenshot_enabled and enabled stay independent settings under Config" do + original_screenshot = Capybara::Screenshot.enabled + original_diff = Capybara::Screenshot::Diff.enabled + + begin + config.screenshot_enabled = true + config.enabled = false + + assert_equal true, Capybara::Screenshot.enabled + assert_equal false, Capybara::Screenshot::Diff.enabled + assert_equal true, config.screenshot_enabled + assert_equal false, config.enabled + ensure + Capybara::Screenshot.enabled = original_screenshot + Capybara::Screenshot::Diff.enabled = original_diff + end + end + + # ADR-008 step 7b moved this precedence rule from + # Capybara::Screenshot.active? into Config#active?, and found it had no + # test at all: replacing the whole expression with a bare `enabled` kept + # all 529 unit tests green. The full truth table is pinned here, through + # both the canonical method and the legacy forwarder, so it cannot move + # again unnoticed. (The canonical file pins the Config#active? column on + # its own, so the rule survives this file's deletion.) + # + # The rule: the Screenshot-side flag wins whenever it was set to anything + # at all; only a nil there falls through to the Diff-side flag. + ACTIVE_TRUTH_TABLE = [ + [true, true, true], + [true, false, true], + [false, true, false], + [false, false, false], + [nil, true, true], + [nil, false, false] + ].freeze + + test "active? gives Screenshot.enabled precedence and only falls through on nil" do + original_screenshot = Capybara::Screenshot.enabled + original_diff = Capybara::Screenshot::Diff.enabled + + ACTIVE_TRUTH_TABLE.each do |screenshot_enabled, enabled, expected| + config.screenshot_enabled = screenshot_enabled + config.enabled = enabled + context = "screenshot_enabled=#{screenshot_enabled.inspect}, enabled=#{enabled.inspect}" + + assert_equal expected, !!config.active?, "Config#active? with #{context}" + assert_equal expected, !!Capybara::Screenshot.active?, "Capybara::Screenshot.active? with #{context}" + end + ensure + Capybara::Screenshot.enabled = original_screenshot + Capybara::Screenshot::Diff.enabled = original_diff + end + + test "writing root through config round-trips through the same Pathname coercion" do + original = Capybara::Screenshot.root + + begin + config.root = "/tmp" + + assert_equal Pathname("/tmp"), Capybara::Screenshot.root + assert_equal Pathname("/tmp"), config.root + ensure + Capybara::Screenshot.root = original + end + end + + test "SnapDiff.configure lets callers set values through the yielded config" do + original = Capybara::Screenshot::Diff.tolerance + + begin + SnapDiff.configure { |c| c.tolerance = 0.0321 } + assert_equal 0.0321, Capybara::Screenshot::Diff.tolerance + ensure + Capybara::Screenshot::Diff.tolerance = original + end + end + + test "SnapDiff.start (v1-style two-arg yield) and SnapDiff.configure (single Config yield) coexist" do + diff_yielded = [] + SnapDiff.start { |screenshot, diff| diff_yielded << [screenshot, diff] } + assert_equal [[Capybara::Screenshot, Capybara::Screenshot::Diff]], diff_yielded + + config_yielded = [] + SnapDiff.configure { |c| config_yielded << c } + assert_equal [config], config_yielded + end +end diff --git a/test/legacy/legacy_config_default_timing_test.rb b/test/legacy/legacy_config_default_timing_test.rb new file mode 100644 index 00000000..7640ae81 --- /dev/null +++ b/test/legacy/legacy_config_default_timing_test.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "test_helper" +require "open3" +require "unit/config_default_timing_test" # single source of truth for the probe scripts + +# LEGACY SURFACE (test/legacy/, see the Rakefile). +# +# The v1 half of config_default_timing_test.rb. Two claims, both about the +# old entry points and the old accessor view, both deleted in 3.0: +# +# 1. every legacy entry point produces the SAME require-time defaults and +# the same freezing/liveness behaviour as the canonical ones -- proved by +# re-running the canonical file's probe scripts verbatim under a v1 +# PROBE_ENTRY, so the two files can never drift; +# 2. every mapped setting reads identically through SnapDiff.config and +# through its legacy mattr_accessor. Together with (1) that is exactly +# what the old `check_both` asserted: the value is right via config, and +# the two surfaces cannot fork. +class LegacyConfigDefaultTimingTest < ActiveSupport::TestCase + ENTRY_POINTS = %w[ + capybara_screenshot_diff + capybara_screenshot_diff/minitest + capybara/screenshot/diff + ].freeze + + def run_probe(script, env) + out, status = Open3.capture2e(env, RbConfig.ruby, "-Ilib", "-e", script) + + assert status.success?, "probe failed:\n#{out}" + end + + BOTH_SURFACES_SCRIPT = ConfigDefaultTimingTest::CHECK_HELPER + <<~'RUBY' + require ENV.fetch("PROBE_ENTRY") + + SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| + check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name)) + end + RUBY + + ENTRY_POINTS.each do |entry| + test "#{entry}: defaults snapshot matches; ENV/pwd frozen at require, wait live" do + run_probe(ConfigDefaultTimingTest::SNAPSHOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) + end + + test "#{entry}: CI=1 before require turns fail_if_new on; unset after require does not turn it off" do + run_probe(ConfigDefaultTimingTest::CI_SET_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => "1"}) + end + + test "#{entry}: Rails.root defined before require wins; reassigning it after require is not seen" do + run_probe(ConfigDefaultTimingTest::RAILS_ROOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) + end + + test "#{entry}: every mapped setting reads the same through config and its mattr_accessor" do + run_probe(BOTH_SURFACES_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) + end + end +end diff --git a/test/legacy/legacy_entry_point_probe_test.rb b/test/legacy/legacy_entry_point_probe_test.rb new file mode 100644 index 00000000..a5c17fcc --- /dev/null +++ b/test/legacy/legacy_entry_point_probe_test.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require "test_helper" +require "unit/support_load_probe_test" # single source of truth for the subprocess probe + +# LEGACY SURFACE (test/legacy/, see the Rakefile). +# +# The v1 half of support_load_probe_test.rb: what the OLD entry points must +# still provide. Names and constants restored verbatim -- every assertion +# here is about a name 3.0 deletes, so repointing them at SnapDiff would +# have quietly turned this file into a duplicate of the canonical one. +class LegacyEntryPointProbeTest < ActiveSupport::TestCase + # Alias-completeness probe (the f89cea2 bug class): each documented entry + # point must define its advertised constants when it is the ONLY require — + # the acyclic redesign once narrowed capybara_screenshot_diff/minitest so + # consumers lost CapybaraScreenshotDiff::DSL, and only one CI matrix leg + # noticed. capybara_screenshot_diff/cucumber is not probed: it calls + # World(...) at load, which only exists inside cucumber's runtime context. + # Documented user-facing constants that must stay EAGER (see + # snap_diff/legacy_shims.rb's exception list): const_defined? never + # triggers const_missing, so a lazy shim makes `defined?` feature + # detection in adopter code silently return nil. + EAGER_USER_FACING = %w[ + Capybara::Screenshot::Diff::Reporters::Default + Capybara::Screenshot::Diff::Comparison + ].freeze + + # The subset of EAGER_USER_FACING that must resolve under EVERY entry + # point, canonical ones included -- these are read directly (a version + # string, a struct), not just feature-detected, so a canonical-only app + # still hits them. The test above only covers the four legacy entries, + # which is how VERSION silently disappeared from six entry points when the + # core stopped requiring capybara/screenshot/diff/version.rb: nothing + # loaded the forwarder that assigned it, and const_missing does not fire + # for a constant legacy_shims deliberately leaves out of its map. + EAGER_EVERYWHERE = %w[ + Capybara::Screenshot::Diff::VERSION + Capybara::Screenshot::Diff::Comparison + ].freeze + + ENTRY_POINTS = { + "capybara_screenshot_diff" => %w[ + CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff + ] + EAGER_USER_FACING, + "capybara_screenshot_diff/minitest" => %w[ + CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions + Capybara::Screenshot::Os Capybara::Screenshot::Diff + ] + EAGER_USER_FACING, + "capybara_screenshot_diff/rspec" => %w[ + CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff + ] + EAGER_USER_FACING, + "capybara-screenshot-diff" => %w[ + CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions + Capybara::Screenshot::Os Capybara::Screenshot::Diff + ] + EAGER_USER_FACING + }.freeze + + test "every documented entry point defines its advertised constants standalone" do + failures = ENTRY_POINTS.filter_map do |entry, constants| + probe(entry, <<~RUBY) + require #{entry.inspect} + missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) } + abort("missing: \#{missing.join(", ")}") unless missing.empty? + RUBY + end + + assert_empty failures, <<~MSG + Entry point(s) no longer provide their advertised constants standalone: + + #{failures.join("\n")} + MSG + end + + # The legacy entries had the mirror image of the beta3 canonical hole: + # some of them stopped loading the umbrella, so CapybaraScreenshotDiff.verify + # and friends vanished while `defined?(CapybaraScreenshotDiff)` still passed. + LEGACY_SESSION_SURFACE = %w[ + verify reset reporters finalize_reporters! assertions registry + pending_screenshots_message + ].freeze + + LEGACY_ENTRY_POINTS = %w[ + capybara-screenshot-diff + snap_diff-capybara + capybara_screenshot_diff + capybara_screenshot_diff/minitest + capybara_screenshot_diff/rspec + capybara_screenshot_diff/cucumber + capybara_screenshot_diff/static + capybara/screenshot/diff + capybara/screenshot/diff/cucumber + ].freeze + + test "every legacy entry point keeps the CapybaraScreenshotDiff session surface" do + failures = LEGACY_ENTRY_POINTS.filter_map do |entry| + probe(entry, <<~RUBY) + require #{entry.inspect} + missing = #{LEGACY_SESSION_SURFACE.inspect}.reject { |m| CapybaraScreenshotDiff.respond_to?(m) } + abort("missing: \#{missing.join(", ")}") unless missing.empty? + RUBY + end + + assert_empty failures, <<~MSG + Legacy entry point(s) leave CapybaraScreenshotDiff half-present + (the module answers `defined?` but not its own session methods): + + #{failures.join("\n")} + MSG + end + + # Every documented entry point, canonical and legacy. capybara_screenshot_diff/dsl + # is listed only here: it is not in ENTRY_POINTS or LEGACY_ENTRY_POINTS, which + # is exactly why it was the legacy entry that lost VERSION unnoticed. + ALL_ENTRY_POINTS = ( + SupportLoadProbeTest::CANONICAL_ENTRY_POINTS.keys + LEGACY_ENTRY_POINTS + %w[capybara_screenshot_diff/dsl] + ).uniq.freeze + + test "the eager user-facing constants resolve under every entry point" do + failures = ALL_ENTRY_POINTS.filter_map do |entry| + probe(entry, <<~RUBY) + require #{entry.inspect} + missing = #{EAGER_EVERYWHERE.inspect}.reject { |c| Object.const_defined?(c) } + abort("missing: \#{missing.join(", ")}") unless missing.empty? + RUBY + end + + assert_empty failures, <<~MSG + Entry point(s) no longer resolve constants that are supposed to be eager + everywhere. `defined?` returns nil for these and const_missing does not + fire, so adopter feature detection fails silently: + + #{failures.join("\n")} + MSG + end + + private + + def probe(entry, script) + SupportLoadProbeTest.probe(entry, script) + end +end diff --git a/test/unit/config_default_timing_test.rb b/test/unit/config_default_timing_test.rb index 59e8977e..ad872e54 100644 --- a/test/unit/config_default_timing_test.rb +++ b/test/unit/config_default_timing_test.rb @@ -7,26 +7,25 @@ # documented entry point, in fresh subprocesses (test_helper preloads the # whole gem, so only a subprocess can observe require-time behavior). # -# Current behavior being pinned (the coming storage inversion must not -# shift any of these): +# Current behavior being pinned: # -# - mattr_accessor block defaults (fail_if_new from ENV["CI"], root from -# Rails.root/pwd) are evaluated ONCE, at class-body eval time, when -# config_legacy.rb is first required. Mutating ENV, cwd, or Rails.root -# after the require -- even before the first read -- must NOT change the -# value. A refactor that turns any of these into a lazy (read-time) -# default, memoized or not, goes red here. -# - Diff.default_options[:wait] is the opposite: it reads +# - the ENV/pwd-derived defaults (fail_if_new from ENV["CI"], root from +# Rails.root/pwd) are evaluated ONCE, when snap_diff/config.rb is first +# required. Mutating ENV, cwd, or Rails.root after the require -- even +# before the first read -- must NOT change the value. A refactor that +# turns any of these into a lazy (read-time) default, memoized or not, +# goes red here. +# - default_options[:wait] is the opposite: it reads # Capybara.default_max_wait_time at CALL time, live, every call. # -# Value snapshots are asserted through BOTH surfaces (SnapDiff.config.x -# and the legacy mattr_accessor) so the inversion can't silently fork them. +# Canonical entry points only, read through SnapDiff.config only. The v1 +# entry points and the "both surfaces agree" half re-run these same scripts +# from test/legacy/legacy_config_default_timing_test.rb, which is deleted +# with the v1 trees in 3.0. class ConfigDefaultTimingTest < ActiveSupport::TestCase ENTRY_POINTS = %w[ - capybara_screenshot_diff - capybara_screenshot_diff/minitest snap_diff - capybara/screenshot/diff + snap_diff/integrations/minitest ].freeze def run_probe(script, env) @@ -40,11 +39,6 @@ def check(name, expected, actual) return if expected == actual abort("#{name}: expected #{expected.inspect}, got #{actual.inspect}") end - - def check_both(name, expected, mod, mattr) - check("#{mod}.#{mattr}", expected, mod.public_send(mattr)) - check("SnapDiff.config.#{name}", expected, SnapDiff.config.public_send(name)) - end RUBY # Probe A: defaults snapshot without CI/Rails + require-time freezing of @@ -62,49 +56,42 @@ def check_both(name, expected, mod, mattr) require "tmpdir" Dir.chdir(Dir.tmpdir) - # 1) Every mapped setting reads the same through both surfaces. - SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| - check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name)) - end - - # 2) Expected default values (CI unset, no Rails, at require time). - # fail_if_new false / root == launch pwd also pin require-time - # evaluation: ENV["CI"] and cwd were changed above, pre-first-read. - screenshot = Capybara::Screenshot - diff = Capybara::Screenshot::Diff + # Expected default values (CI unset, no Rails, at require time). + # fail_if_new false / root == launch pwd also pin require-time + # evaluation: ENV["CI"] and cwd were changed above, pre-first-read. { - add_driver_path: [nil, screenshot, :add_driver_path], - add_os_path: [nil, screenshot, :add_os_path], - blur_active_element: [true, screenshot, :blur_active_element], - screenshot_enabled: [nil, screenshot, :enabled], - hide_caret: [true, screenshot, :hide_caret], - disable_animations: [nil, screenshot, :disable_animations], - root: [launch_pwd, screenshot, :root], - stability_time_limit: [nil, screenshot, :stability_time_limit], - window_size: [nil, screenshot, :window_size], - save_path: ["doc/screenshots", screenshot, :save_path], - use_lfs: [nil, screenshot, :use_lfs], - screenshot_format: ["png", screenshot, :screenshot_format], - capybara_screenshot_options: [{}, screenshot, :capybara_screenshot_options], - delayed: [true, diff, :delayed], - area_size_limit: [nil, diff, :area_size_limit], - fail_if_new: [false, diff, :fail_if_new], - pending_if_new: [false, diff, :pending_if_new], - fail_on_difference: [true, diff, :fail_on_difference], - color_distance_limit: [nil, diff, :color_distance_limit], - enabled: [true, diff, :enabled], - shift_distance_limit: [nil, diff, :shift_distance_limit], - skip_area: [nil, diff, :skip_area], - driver: [:auto, diff, :driver], - tolerance: [nil, diff, :tolerance], - perceptual_threshold: [nil, diff, :perceptual_threshold], - screenshoter: [SnapDiff::Screenshoter, diff, :screenshoter], - manager: [SnapDiff::SnapManager, diff, :manager] - }.each do |name, (expected, mod, mattr)| - check_both(name, expected, mod, mattr) + add_driver_path: nil, + add_os_path: nil, + blur_active_element: true, + screenshot_enabled: nil, + hide_caret: true, + disable_animations: nil, + root: launch_pwd, + stability_time_limit: nil, + window_size: nil, + save_path: "doc/screenshots", + use_lfs: nil, + screenshot_format: "png", + capybara_screenshot_options: {}, + delayed: true, + area_size_limit: nil, + fail_if_new: false, + pending_if_new: false, + fail_on_difference: true, + color_distance_limit: nil, + enabled: true, + shift_distance_limit: nil, + skip_area: nil, + driver: :auto, + tolerance: nil, + perceptual_threshold: nil, + screenshoter: SnapDiff::Screenshoter, + manager: SnapDiff::SnapManager + }.each do |name, expected| + check("SnapDiff.config.#{name}", expected, SnapDiff.config.public_send(name)) end - # 3) Capybara-coupled wait is read at CALL time (live), not frozen. + # Capybara-coupled wait is read at CALL time (live), not frozen. Capybara.default_max_wait_time = 42.5 check("default_options[:wait] follows Capybara.default_max_wait_time set after require", 42.5, SnapDiff.config.default_options[:wait]) @@ -116,7 +103,7 @@ def check_both(name, expected, mod, mattr) CI_SET_SCRIPT = CHECK_HELPER + <<~RUBY require ENV.fetch("PROBE_ENTRY") ENV.delete("CI") - check_both(:fail_if_new, true, Capybara::Screenshot::Diff, :fail_if_new) + check(:fail_if_new, true, SnapDiff.config.fail_if_new) RUBY # Probe C: a Rails module with .root defined BEFORE the require wins over @@ -134,11 +121,11 @@ class << self require ENV.fetch("PROBE_ENTRY") Rails.root = Pathname("/fake-rails-root-after-require") - check_both(:root, Pathname("/fake-rails-root-at-require"), Capybara::Screenshot, :root) + check(:root, Pathname("/fake-rails-root-at-require"), SnapDiff.config.root) RUBY ENTRY_POINTS.each do |entry| - test "#{entry}: defaults snapshot matches through both surfaces; ENV/pwd frozen at require, wait live" do + test "#{entry}: defaults snapshot matches; ENV/pwd frozen at require, wait live" do run_probe(SNAPSHOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) end diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb index 6f282ce1..bbafabe1 100644 --- a/test/unit/diff_test.rb +++ b/test/unit/diff_test.rb @@ -20,7 +20,6 @@ class DiffTest < ActiveSupport::TestCase SnapDiff.config.window_size = [80, 80] end - include Capybara::Screenshot::Diff include SnapDiff::Minitest::Assertions include DSLStub @@ -35,7 +34,7 @@ class DiffTest < ActiveSupport::TestCase end test "has a version number" do - refute_nil ::Capybara::Screenshot::Diff::VERSION + refute_nil SnapDiff::VERSION end test "updates screenshot group name" do @@ -141,7 +140,6 @@ class DiffTest < ActiveSupport::TestCase end class SampleMiniTestCase < ActiveSupport::TestCase - include Capybara::Screenshot::Diff include SnapDiff::Minitest::Assertions # NOTE: we need to add `_` as prefix to skip this test from auto-run @@ -178,7 +176,6 @@ def teardown SnapDiff.reset end - include Capybara::Screenshot::Diff include NonMinitest::Assertions def _test_sample_screenshot_error @@ -201,7 +198,6 @@ class ScreenshotFormatTest < ActiveSupport::TestCase @orig_screenshot_format = SnapDiff.config.screenshot_format end - include Capybara::Screenshot::Diff include DSLStub include SnapDiff::Minitest::Assertions diff --git a/test/unit/drivers/chunky_png_driver_test.rb b/test/unit/drivers/chunky_png_driver_test.rb index 809606f7..30178337 100644 --- a/test/unit/drivers/chunky_png_driver_test.rb +++ b/test/unit/drivers/chunky_png_driver_test.rb @@ -6,7 +6,7 @@ require "support/driver_contract_tests" # Nested in the canonical SnapDiff::Drivers namespace: reopening the old -# SnapDiff::Drivers path would define a fresh Drivers +# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers # module there, shadowing the v2 step 6 lazy const_missing forwarder. module SnapDiff module Drivers diff --git a/test/unit/drivers/vips_driver_test.rb b/test/unit/drivers/vips_driver_test.rb index f10e86fa..abf22f53 100644 --- a/test/unit/drivers/vips_driver_test.rb +++ b/test/unit/drivers/vips_driver_test.rb @@ -6,7 +6,7 @@ require "snap_diff/drivers/vips_driver" if defined?(Vips) # Nested in the canonical SnapDiff::Drivers namespace: reopening the old -# SnapDiff::Drivers path would define a fresh Drivers +# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers # module there, shadowing the v2 step 6 lazy const_missing forwarder. module SnapDiff module Drivers diff --git a/test/unit/dsl_test.rb b/test/unit/dsl_test.rb index ee3158e7..39015f35 100644 --- a/test/unit/dsl_test.rb +++ b/test/unit/dsl_test.rb @@ -186,11 +186,11 @@ def self.screenshot(*, **) test "SnapDiff.reset clears new_screenshots" do SnapDiff::Vcs.stub(:checkout_vcs, false) do screenshot "a" - assert_predicate CapybaraScreenshotDiff, :new_screenshots_present? + assert_predicate SnapDiff.session, :new_screenshots_present? SnapDiff.reset - assert_not_predicate CapybaraScreenshotDiff, :new_screenshots_present? + assert_not_predicate SnapDiff.session, :new_screenshots_present? assert_empty SnapDiff.session.new_screenshots end end diff --git a/test/unit/image_compare_test.rb b/test/unit/image_compare_test.rb index ae631046..f759c9f2 100644 --- a/test/unit/image_compare_test.rb +++ b/test/unit/image_compare_test.rb @@ -78,7 +78,7 @@ class ImageCompareTest < ActiveSupport::TestCase test "#initialize with :auto driver raises error when no drivers available" do # Canonical stubbing point since the detected-drivers list moved to # SnapDiff::Drivers (3.0 readiness). The legacy - # SnapDiff::Drivers::AVAILABLE_DRIVERS is now an eager + # Capybara::Screenshot::Diff::AVAILABLE_DRIVERS is now an eager # same-object ALIAS of this constant, so stubbing the old name only # rebinds the alias and no longer reaches the core. SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) do diff --git a/test/unit/reporters_mutex_test.rb b/test/unit/reporters_mutex_test.rb index c6eba894..07010acd 100644 --- a/test/unit/reporters_mutex_test.rb +++ b/test/unit/reporters_mutex_test.rb @@ -63,7 +63,7 @@ class ReportersMutexTest < ActiveSupport::TestCase assertions = [:some, :assertions] assert_nothing_raised do - CapybaraScreenshotDiff.send(:notify_reporters, assertions) + SnapDiff::Reporting.notify(assertions) end assert_equal [[:original, assertions]], received diff --git a/test/unit/snap_diff_config_test.rb b/test/unit/snap_diff_config_test.rb index 14bacd7e..0c236d0e 100644 --- a/test/unit/snap_diff_config_test.rb +++ b/test/unit/snap_diff_config_test.rb @@ -2,6 +2,11 @@ require "test_helper" +# The Config-only half. Everything about the v1 mattr_accessor VIEW of this +# same storage -- LegacyShims::CONFIG_MAPPING's completeness and the +# write-through-either-surface round trips -- lives in +# test/legacy/legacy_config_accessors_test.rb and is deleted with the v1 +# trees in 3.0. What is here has to keep standing on its own after that. class SnapDiffConfigTest < ActiveSupport::TestCase def config SnapDiff.config @@ -15,127 +20,48 @@ def config assert_same config, SnapDiff.config end - # Reflection-based completeness check, reworked for the ADR-008 storage - # inversion (the old version derived settings from mattr class variables, - # which no longer exist). Two directions: - # - # (a) every singleton writer on the legacy modules is a mapped config - # setting -- a future `mattr_accessor :foo` (active_support's ext is - # one require away) or hand-rolled writer would create unmapped - # storage invisible to SnapDiff.config; - # (b) SnapDiff.config stores exactly one ivar per declared setting -- catches - # both an unmapped ivar sneaking into Config and a mapped setting - # whose ivar is missing (which would also silently escape - # test_helper's per-test ivar snapshot/restore). - NON_CONFIG_WRITERS = [].freeze # currently no non-config writer API on the legacy modules - - test "every legacy singleton writer is covered by SnapDiff::LegacyShims::CONFIG_MAPPING" do - covered = SnapDiff::LegacyShims::CONFIG_MAPPING.values - - [Capybara::Screenshot, Capybara::Screenshot::Diff].each do |mod| - writers = mod.singleton_class.public_instance_methods(false).grep(/=\z/) - NON_CONFIG_WRITERS - - assert_operator writers.size, :>, 0, "#{mod} lost all its config writers" - writers.each do |writer| - mattr = writer.to_s.delete_suffix("=").to_sym - - assert_includes covered, [mod, mattr], - "#{mod}.#{writer} is a config writer with no SnapDiff::Config mapping. " \ - "Add an entry to LegacyShims::CONFIG_MAPPING (rename the key if `#{mattr}` collides " \ - "with an existing one, as `enabled` does), or add it to NON_CONFIG_WRITERS " \ - "if it is deliberately not a config setting." - end - end - end - - # The two halves of the split are only safe while they agree: Config - # declares the settings and knows nothing about the legacy holders, - # LegacyShims says which holder each one is exposed on. A setting in one - # and not the other is either storage with no v1 accessor or a v1 - # accessor delegating to a setting that does not exist. - test "LegacyShims::CONFIG_MAPPING covers exactly Config::SETTINGS" do - assert_equal SnapDiff::Config::SETTINGS, SnapDiff::LegacyShims::CONFIG_MAPPING.keys - end - + # Catches both an unmapped ivar sneaking into Config and a declared + # setting whose ivar is missing (which would also silently escape + # test_helper's per-test ivar snapshot/restore). test "SnapDiff.config stores exactly one ivar per declared setting" do assert_equal SnapDiff::Config::SETTINGS.map { |k| :"@#{k}" }.sort, SnapDiff.config.instance_variables.sort end - test "every mapped setting is readable via config and equal to its mattr_accessor's value" do - SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| - expected = mod.public_send(mattr) - actual = config.public_send(name) - - if expected.nil? - assert_nil actual, "config.#{name} should equal #{mod}.#{mattr}" - else - assert_equal expected, actual, "config.#{name} should equal #{mod}.#{mattr}" - end - end - end - - test "writing fail_if_new via the old mattr_accessor is visible via config, and back" do - original = SnapDiff.config.fail_if_new - - begin - SnapDiff.config.fail_if_new = true - assert_equal true, config.fail_if_new - - config.fail_if_new = false - assert_equal false, SnapDiff.config.fail_if_new - ensure - SnapDiff.config.fail_if_new = original - end - end - - test "writing window_size via the old mattr_accessor is visible via config, and back" do - original = SnapDiff.config.window_size - - begin - SnapDiff.config.window_size = [1280, 1024] - assert_equal [1280, 1024], config.window_size - - config.window_size = [800, 600] - assert_equal [800, 600], SnapDiff.config.window_size - ensure - SnapDiff.config.window_size = original - end - end - - # SnapDiff.config.screenshot_enabled and SnapDiff.config.enabled are - # two independent settings that happen to share a bare name in their own - # modules (see SnapDiff.config.active?, which reads both). Config is - # flat, so it cannot expose two attributes both called `enabled` -- the - # Screenshot-side one is renamed `screenshot_enabled`. This test proves - # the rename didn't accidentally collapse them into one shared value. + # screenshot_enabled and enabled are two independent settings that + # happened to share a bare name in their own v1 modules (see #active?, + # which reads both). Config is flat, so it cannot expose two attributes + # both called `enabled` -- the Screenshot-side one is renamed + # `screenshot_enabled`. This proves the rename didn't accidentally + # collapse them into one shared value. test "screenshot_enabled and enabled stay independent settings under Config" do - original_screenshot = SnapDiff.config.screenshot_enabled - original_diff = SnapDiff.config.enabled + original_screenshot = config.screenshot_enabled + original_diff = config.enabled begin config.screenshot_enabled = true config.enabled = false - assert_equal true, SnapDiff.config.screenshot_enabled - assert_equal false, SnapDiff.config.enabled assert_equal true, config.screenshot_enabled assert_equal false, config.enabled + + config.screenshot_enabled = false + config.enabled = true + + assert_equal false, config.screenshot_enabled + assert_equal true, config.enabled ensure - SnapDiff.config.screenshot_enabled = original_screenshot - SnapDiff.config.enabled = original_diff + config.screenshot_enabled = original_screenshot + config.enabled = original_diff end end - # ADR-008 step 7b moved this precedence rule from - # SnapDiff.config.active? into Config#active?, and found it had no - # test at all: replacing the whole expression with a bare `enabled` kept - # all 529 unit tests green. The full truth table is pinned here, through - # both the canonical method and the legacy forwarder, so it cannot move - # again unnoticed. + # ADR-008 step 7b moved this precedence rule into Config#active?, and + # found it had no test at all: replacing the whole expression with a bare + # `enabled` kept all 529 unit tests green. # - # The rule: the Screenshot-side flag wins whenever it was set to anything - # at all; only a nil there falls through to the Diff-side flag. + # The rule: the screenshot-side flag wins whenever it was set to anything + # at all; only a nil there falls through to the diff-side flag. ACTIVE_TRUTH_TABLE = [ [true, true, true], [true, false, true], @@ -145,9 +71,9 @@ def config [nil, false, false] ].freeze - test "active? gives Screenshot.enabled precedence and only falls through on nil" do - original_screenshot = SnapDiff.config.screenshot_enabled - original_diff = SnapDiff.config.enabled + test "active? gives screenshot_enabled precedence and only falls through on nil" do + original_screenshot = config.screenshot_enabled + original_diff = config.enabled ACTIVE_TRUTH_TABLE.each do |screenshot_enabled, enabled, expected| config.screenshot_enabled = screenshot_enabled @@ -155,23 +81,21 @@ def config context = "screenshot_enabled=#{screenshot_enabled.inspect}, enabled=#{enabled.inspect}" assert_equal expected, !!config.active?, "Config#active? with #{context}" - assert_equal expected, !!SnapDiff.config.active?, "SnapDiff.config.active? with #{context}" end ensure - SnapDiff.config.screenshot_enabled = original_screenshot - SnapDiff.config.enabled = original_diff + config.screenshot_enabled = original_screenshot + config.enabled = original_diff end test "writing root through config round-trips through the same Pathname coercion" do - original = SnapDiff.config.root + original = config.root begin config.root = "/tmp" - assert_equal Pathname("/tmp"), SnapDiff.config.root assert_equal Pathname("/tmp"), config.root ensure - SnapDiff.config.root = original + config.root = original end end @@ -183,23 +107,13 @@ def config end test "SnapDiff.configure lets callers set values through the yielded config" do - original = SnapDiff.config.tolerance + original = config.tolerance begin SnapDiff.configure { |c| c.tolerance = 0.0321 } - assert_equal 0.0321, SnapDiff.config.tolerance + assert_equal 0.0321, config.tolerance ensure - SnapDiff.config.tolerance = original + config.tolerance = original end end - - test "SnapDiff.start (v1-style two-arg yield) and SnapDiff.configure (single Config yield) coexist" do - diff_yielded = [] - SnapDiff.start { |screenshot, diff| diff_yielded << [screenshot, diff] } - assert_equal [[Capybara::Screenshot, Capybara::Screenshot::Diff]], diff_yielded - - config_yielded = [] - SnapDiff.configure { |c| config_yielded << c } - assert_equal [config], config_yielded - end end diff --git a/test/unit/support_load_probe_test.rb b/test/unit/support_load_probe_test.rb index 53d71485..c04c9cb4 100644 --- a/test/unit/support_load_probe_test.rb +++ b/test/unit/support_load_probe_test.rb @@ -10,9 +10,9 @@ # own requires still loads fine in most suite runs and only breaks in the CI # matrix cell with a different load order — exactly how # setup_capybara_drivers.rb broke on selenium_chrome_headless + vips (it used -# SnapDiff::Os without requiring it, fixed in b7ada5e). This test -# requires every test/support file in a bare subprocess with only capybara -# core preloaded. Scope: it catches missing requires for constants referenced +# SnapDiff::Os without requiring it, fixed in b7ada5e). This test requires +# every test/support file in a bare subprocess with only capybara core +# preloaded. Scope: it catches missing requires for constants referenced # AT LOAD TIME under this process's env; references hidden behind env guards # (e.g. CAPYBARA_DRIVER branches not taken here) or inside method bodies # still escape it. @@ -22,6 +22,12 @@ # active_support/concern in dsl_stub and driver_contract_tests, the gem's own # files elsewhere). If a future support file legitimately needs more context # than "capybara is loaded", list it in SKIP with the reason. +# +# The v1 entry points -- their advertised constants, the +# CapybaraScreenshotDiff session surface, and the eager user-facing +# constants under the old names -- are probed the same way from +# test/legacy/legacy_entry_point_probe_test.rb, which is deleted with the v1 +# trees in 3.0. class SupportLoadProbeTest < ActiveSupport::TestCase SKIP = { # "support/example" => "why it cannot load bare" @@ -50,76 +56,12 @@ class SupportLoadProbeTest < ActiveSupport::TestCase MSG end - # Alias-completeness probe (the f89cea2 bug class): each documented entry - # point must define its advertised constants when it is the ONLY require — - # the acyclic redesign once narrowed capybara_screenshot_diff/minitest so - # consumers lost SnapDiff::DSL, and only one CI matrix leg - # noticed. capybara_screenshot_diff/cucumber is not probed: it calls - # World(...) at load, which only exists inside cucumber's runtime context. - # Documented user-facing constants that must stay EAGER (see - # snap_diff/legacy_shims.rb's exception list): const_defined? never - # triggers const_missing, so a lazy shim makes `defined?` feature - # detection in adopter code silently return nil. - EAGER_USER_FACING = %w[ - SnapDiff::Reporters::Default - SnapDiff::Comparison::Images - ].freeze - - # The subset of EAGER_USER_FACING that must resolve under EVERY entry - # point, canonical ones included -- these are read directly (a version - # string, a struct), not just feature-detected, so a canonical-only app - # still hits them. The test above only covers the four legacy entries, - # which is how VERSION silently disappeared from six entry points when the - # core stopped requiring capybara/screenshot/diff/version.rb: nothing - # loaded the forwarder that assigned it, and const_missing does not fire - # for a constant legacy_shims deliberately leaves out of its map. - EAGER_EVERYWHERE = %w[ - SnapDiff::VERSION - SnapDiff::Comparison::Images - ].freeze - - ENTRY_POINTS = { - "capybara_screenshot_diff" => %w[ - SnapDiff::DSL SnapDiff::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara_screenshot_diff/minitest" => %w[ - SnapDiff::DSL SnapDiff::Minitest::Assertions - SnapDiff::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara_screenshot_diff/rspec" => %w[ - SnapDiff::DSL SnapDiff::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara-screenshot-diff" => %w[ - SnapDiff::DSL SnapDiff::Minitest::Assertions - SnapDiff::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING - }.freeze - - test "every documented entry point defines its advertised constants standalone" do - failures = ENTRY_POINTS.filter_map do |entry, constants| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Entry point(s) no longer provide their advertised constants standalone: - - #{failures.join("\n")} - MSG - end - # --- beta3 blockers: entry points must load a COMPLETE surface --- # # The canonical `snap_diff/*` requires never loaded snap_diff.rb itself, # so the docs' own quick start (`require "snap_diff/integrations/minitest"`) # left SnapDiff.configure/.start/.compare undefined, SnapDiff::VERSION - # unresolvable, and the dual-install guard silent. The legacy entries had - # the mirror-image hole: some of them stopped loading the umbrella, so - # SnapDiff.session.verify and friends vanished while - # `defined?(CapybaraScreenshotDiff)` still passed. + # unresolvable, and the dual-install guard silent. # SnapDiff.serve is deliberately absent here: docs/snapdiff.md's object # map gates it behind its own `require "snap_diff/static"` (which pulls @@ -128,35 +70,23 @@ class SupportLoadProbeTest < ActiveSupport::TestCase config configure start compare session reset pending_screenshots_message ].freeze + # snap_diff-capybara is the canonical gem's Bundler entry point, so it is + # probed here rather than with the v1 ones: 3.0 keeps it (repointed at + # snap_diff/integrations/minitest), it just stops carrying the + # CapybaraScreenshotDiff surface. CANONICAL_ENTRY_POINTS = { "snap_diff" => CANONICAL_SURFACE, "snap_diff/dsl" => CANONICAL_SURFACE, "snap_diff/integrations/minitest" => CANONICAL_SURFACE, "snap_diff/integrations/rspec" => CANONICAL_SURFACE, "snap_diff/integrations/cucumber" => CANONICAL_SURFACE, - "snap_diff/static" => CANONICAL_SURFACE + %w[serve] + "snap_diff/static" => CANONICAL_SURFACE + %w[serve], + "snap_diff-capybara" => CANONICAL_SURFACE }.freeze - LEGACY_SESSION_SURFACE = %w[ - verify reset reporters finalize_reporters! assertions registry - pending_screenshots_message - ].freeze - - LEGACY_ENTRY_POINTS = %w[ - capybara-screenshot-diff - snap_diff-capybara - capybara_screenshot_diff - capybara_screenshot_diff/minitest - capybara_screenshot_diff/rspec - capybara_screenshot_diff/cucumber - capybara_screenshot_diff/static - capybara/screenshot/diff - capybara/screenshot/diff/cucumber - ].freeze - test "every canonical snap_diff entry point loads the full SnapDiff surface" do failures = CANONICAL_ENTRY_POINTS.filter_map do |entry, methods| - probe(entry, <<~RUBY) + self.class.probe(entry, <<~RUBY) require #{entry.inspect} missing = #{methods.inspect}.reject { |m| SnapDiff.respond_to?(m) } missing << "VERSION" unless defined?(SnapDiff::VERSION) @@ -173,7 +103,7 @@ class SupportLoadProbeTest < ActiveSupport::TestCase test "every canonical snap_diff entry point runs the dual-install guard" do failures = CANONICAL_ENTRY_POINTS.keys.filter_map do |entry| - probe(entry, <<~RUBY) + self.class.probe(entry, <<~RUBY) require "rubygems" %w[capybara-screenshot-diff snap_diff-capybara].each do |name| Gem.loaded_specs[name] ||= Gem::Specification.new(name, "0.0.0") @@ -194,50 +124,6 @@ class SupportLoadProbeTest < ActiveSupport::TestCase MSG end - test "every legacy entry point keeps the CapybaraScreenshotDiff session surface" do - failures = LEGACY_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{LEGACY_SESSION_SURFACE.inspect}.reject { |m| CapybaraScreenshotDiff.respond_to?(m) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Legacy entry point(s) leave CapybaraScreenshotDiff half-present - (the module answers `defined?` but not its own session methods): - - #{failures.join("\n")} - MSG - end - - # Every documented entry point, canonical and legacy. capybara_screenshot_diff/dsl - # is listed only here: it is not in ENTRY_POINTS or LEGACY_ENTRY_POINTS, which - # is exactly why it was the legacy entry that lost VERSION unnoticed. - ALL_ENTRY_POINTS = ( - CANONICAL_ENTRY_POINTS.keys + LEGACY_ENTRY_POINTS + %w[capybara_screenshot_diff/dsl] - ).uniq.freeze - - test "the eager user-facing constants resolve under every entry point" do - failures = ALL_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{EAGER_EVERYWHERE.inspect}.reject { |c| Object.const_defined?(c) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Entry point(s) no longer resolve constants that are supposed to be eager - everywhere. `defined?` returns nil for these and const_missing does not - fire, so adopter feature detection fails silently: - - #{failures.join("\n")} - MSG - end - - private - # cucumber's World/Before/After/AfterAll only exist inside its runtime, so # a bare probe of a cucumber entry has to supply them or the require dies # before it reaches what is under test. @@ -250,7 +136,10 @@ def AfterAll(*) = nil # Runs +script+ in a fresh process with only lib/ on the load path. # Returns nil on success, a failure description otherwise. - def probe(entry, script) + # + # Public, and deliberately so: test/legacy/legacy_entry_point_probe_test.rb + # reuses it rather than keeping a second copy that would drift. + def self.probe(entry, script) project_root = File.expand_path("../..", __dir__) preamble = entry.include?("cucumber") ? CUCUMBER_RUNTIME_STUB : "" out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", preamble + script, chdir: project_root) From 17225cc342e89b68e785dae3906ffab9caf84526 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:08:42 +0200 Subject: [PATCH 4/5] test: port the canonical claims that only a legacy-surface test was pinning Audit of every assertion moving into test/legacy/, asking: if this file vanished at 3.0, would any CANONICAL behaviour become untested? Two hits, both now duplicated (not moved) into a canonical test -- the v1 originals stay put, they still guard the v1 contract for all of 2.x: - namespace_forwarding_test was the only place proving SnapDiff::Drivers .loaded is ONE hash mutated in place (it asserted the v1 LOADED_DRIVERS constant is that same object, and that registering through it shows up canonically). Utils.find_driver_class_for caches through .loaded, so a copy-returning refactor would break user driver registration silently. -> drivers_test ".loaded is a single hash mutated in place" mutation: `.loaded.dup[...] = ...` -> red, Expected :probe_driver, got nil - the entry-point probe was the only place asserting an entry point defines its advertised CONSTANTS when it is the ONLY require (the f89cea2 bug class) -- but only for the v1 names. -> support_load_probe_test "every canonical entry point defines its advertised constants standalone", same claim over snap_diff/dsl, /integrations/minitest, /integrations/rspec, snap_diff-capybara. Entry-specific, because bare snap_diff carries neither DSL nor reporters by design. mutation: a bogus constant in the list -> red, naming it Also: attempts_reporter_test now requires snap_diff/attempts_reporter -- stable_screenshoter pulls it in lazily and the v1 umbrella was what loaded it eagerly, so it was the one canonical test the deletion actually broke. Judged legacy-only and safe to lose at 3.0: const_missing/eager alias semantics, deprecation warn-once + silencing, CONFIG_MAPPING completeness, the alias-only scan of lib/capybara*, and the CapybaraScreenshotDiff session/reporter forwarders -- every one is about a name 3.0 deletes, and its canonical counterpart is pinned in test/unit/. rake test 574 runs, 1593 assertions, 0F/0E/1S --- test/unit/attempts_reporter_test.rb | 3 +++ test/unit/drivers_test.rb | 16 ++++++++++++ test/unit/support_load_probe_test.rb | 38 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/test/unit/attempts_reporter_test.rb b/test/unit/attempts_reporter_test.rb index 7f512274..c2fb19fc 100644 --- a/test/unit/attempts_reporter_test.rb +++ b/test/unit/attempts_reporter_test.rb @@ -2,6 +2,9 @@ require "test_helper" require "snap_diff" +# Not on any entry point's require path: stable_screenshoter pulls it in +# lazily, at the moment a capture actually goes unstable. +require "snap_diff/attempts_reporter" module SnapDiff # Guard #2 from the v2 core-redesign acceptance contract (D8). diff --git a/test/unit/drivers_test.rb b/test/unit/drivers_test.rb index bd4eaa1d..a6f0bae5 100644 --- a/test/unit/drivers_test.rb +++ b/test/unit/drivers_test.rb @@ -39,6 +39,22 @@ class DriversTest < ActiveSupport::TestCase assert_equal SnapDiff::Drivers::AVAILABLE_DRIVERS, SnapDiff::Drivers.available end + # Ported from namespace_forwarding_test (test/legacy/), which was the only + # place pinning this: it asserted the v1 LOADED_DRIVERS constant is the + # same object as this hash and that a registration through it shows up + # here. The v1 half dies in 3.0; the canonical half -- .loaded is ONE + # memoized hash, mutated in place, so a driver registered into it stays + # registered (Utils.find_driver_class_for caches through it) -- must not. + test ".loaded is a single hash mutated in place, so registrations stick" do + assert_same SnapDiff::Drivers.loaded, SnapDiff::Drivers.loaded + + SnapDiff::Drivers.loaded[:registry_probe] = :probe_driver + + assert_equal :probe_driver, SnapDiff::Drivers.loaded[:registry_probe] + ensure + SnapDiff::Drivers.loaded.delete(:registry_probe) + end + test "detection is reachable under both the canonical and the documented Utils name" do assert_equal SnapDiff::Drivers.detect_available, SnapDiff::Utils.detect_available_drivers end diff --git a/test/unit/support_load_probe_test.rb b/test/unit/support_load_probe_test.rb index c04c9cb4..78457a65 100644 --- a/test/unit/support_load_probe_test.rb +++ b/test/unit/support_load_probe_test.rb @@ -101,6 +101,44 @@ class SupportLoadProbeTest < ActiveSupport::TestCase MSG end + # Ported from the legacy entry-point probe (test/legacy/), which was the + # only place asserting that an entry point defines its advertised + # CONSTANTS when it is the ONLY require -- it just did so for the v1 names + # (CapybaraScreenshotDiff::DSL, Capybara::Screenshot::Os, ...). That is the + # f89cea2 bug class: the acyclic redesign once narrowed an entry point so + # consumers lost the DSL, and only one CI matrix leg noticed. Same claim, + # canonical names, canonical entries -- so 3.0 keeps the guard. + # + # Entry-specific on purpose: bare `snap_diff` deliberately carries neither + # the DSL nor the reporters (see CANONICAL_SURFACE above), so a flat list + # over all entries would be wrong rather than strict. + CANONICAL_ADVERTISED_CONSTANTS = { + "snap_diff/dsl" => %w[SnapDiff::DSL SnapDiff::Os SnapDiff::Comparison], + "snap_diff/integrations/minitest" => %w[ + SnapDiff::DSL SnapDiff::Minitest::Assertions SnapDiff::Os SnapDiff::Comparison + ], + "snap_diff/integrations/rspec" => %w[SnapDiff::DSL SnapDiff::Os SnapDiff::Comparison], + "snap_diff-capybara" => %w[ + SnapDiff::DSL SnapDiff::Minitest::Assertions SnapDiff::Os SnapDiff::Comparison + ] + }.freeze + + test "every canonical entry point defines its advertised constants standalone" do + failures = CANONICAL_ADVERTISED_CONSTANTS.filter_map do |entry, constants| + self.class.probe(entry, <<~RUBY) + require #{entry.inspect} + missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) } + abort("missing: \#{missing.join(", ")}") unless missing.empty? + RUBY + end + + assert_empty failures, <<~MSG + Canonical entry point(s) no longer provide their advertised constants standalone: + + #{failures.join("\n")} + MSG + end + test "every canonical snap_diff entry point runs the dual-install guard" do failures = CANONICAL_ENTRY_POINTS.keys.filter_map do |entry| self.class.probe(entry, <<~RUBY) From 0f003dc2ae9bf3c310a1b12570694706f7682ab7 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:35:16 +0200 Subject: [PATCH 5/5] fix: the canonical gate demanded SnapDiff.start, which 3.0 deletes Caught by independent review: `rake test:canonical` in the deleted tree is 1F, not the 0F I published. CANONICAL_SURFACE listed `start`, applied to all 7 canonical entry points. SnapDiff.start is defined only in lib/snap_diff/legacy_shims.rb:169 and yields the two v1 config holders, so it cannot outlive them (#235 decided this). A canonical gate demanding a method 3.0 deletes is a gate that goes red the day the deletion lands -- and I widened it in e789c8a by adding snap_diff-capybara. My own PR body filed .start under "safe to lose at 3.0". mutation (start put back, deleted tree): require "snap_diff" -> missing: start require "snap_diff/dsl" -> missing: start require "snap_diff/integrations/minitest" -> missing: start ... all 7 entry points .start keeps full coverage on the legacy side: legacy_forwarders_test pins what it yields and that it applies a setting, and a new per-entry-point probe in legacy_entry_point_probe_test pins the availability claim the canonical gate used to make -- for the entries that actually keep it. Also, per review: - "bare require never loads the umbrella" moves to legacy_forwarders_test. Its subject is lib/capybara_screenshot_diff.rb; once 3.0 deletes that file the $LOADED_FEATURES grep is empty by construction and the guard can never fail again. (-1 canonical run: 458 -> 457.) - backtrace_filter_test built synthetic paths under lib/capybara_screenshot_diff/. Pure string inputs to a prefix matcher, so no assertion changes -- but one of them named the real file the filter defaults to, which 3.0 deletes. rake test:unit 547 runs, 1550 assertions, 0F/0E/0S rake test 575 runs, 1595 assertions, 0F/0E/1S rake test:canonical 457 runs, 1288 assertions, 0F/0E/1S ... and 457/1288/0F/0E/1S in the deleted tree, identical. --- test/legacy/legacy_entry_point_probe_test.rb | 17 +++++++++++++ test/legacy/legacy_forwarders_test.rb | 26 ++++++++++++++++++++ test/unit/backtrace_filter_test.rb | 4 +-- test/unit/snap_diff_test.rb | 25 ++++--------------- test/unit/support_load_probe_test.rb | 10 +++++++- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/test/legacy/legacy_entry_point_probe_test.rb b/test/legacy/legacy_entry_point_probe_test.rb index a5c17fcc..52f3b407 100644 --- a/test/legacy/legacy_entry_point_probe_test.rb +++ b/test/legacy/legacy_entry_point_probe_test.rb @@ -108,6 +108,23 @@ class LegacyEntryPointProbeTest < ActiveSupport::TestCase MSG end + # SnapDiff.start moved here out of the canonical CANONICAL_SURFACE gate: it + # is defined in legacy_shims.rb and yields the two v1 config holders, so a + # canonical gate demanding it fails the moment 3.0 deletes them. It is + # still a documented v1 method, so the per-entry-point availability claim + # the canonical gate used to make lives on here -- for the entries that + # actually keep it. (What it yields is pinned in legacy_forwarders_test.) + test "SnapDiff.start is available from every legacy entry point" do + failures = LEGACY_ENTRY_POINTS.filter_map do |entry| + probe(entry, <<~RUBY) + require #{entry.inspect} + abort("SnapDiff.start missing") unless SnapDiff.respond_to?(:start) + RUBY + end + + assert_empty failures, failures.join("\n") + end + # Every documented entry point, canonical and legacy. capybara_screenshot_diff/dsl # is listed only here: it is not in ENTRY_POINTS or LEGACY_ENTRY_POINTS, which # is exactly why it was the legacy entry that lost VERSION unnoticed. diff --git a/test/legacy/legacy_forwarders_test.rb b/test/legacy/legacy_forwarders_test.rb index 5ed3b3e7..b93ac092 100644 --- a/test/legacy/legacy_forwarders_test.rb +++ b/test/legacy/legacy_forwarders_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "test_helper" +require "open3" # The shared harness loads canonical entry points only, so a legacy-surface # test pulls in the v1 entry itself -- the require goes with the file in 3.0. require "capybara_screenshot_diff" @@ -74,6 +75,31 @@ class LegacyForwardersTest < ActiveSupport::TestCase SnapDiff.config.root = original_root end + # Acyclicity contract (the #208 deadlock-class fix): the lean + # `require "snap_diff"` entry must NEVER pull the umbrella + # capybara_screenshot_diff.rb back in. The old autoload wiring had + # snap_diff <-> capybara_screenshot_diff requiring each other, which + # produced load-order deadlocks/partially-initialized constants; #208 + # broke the cycle, but until now only discipline guarded it -- a probe + # that reintroduced the cycle left the whole suite green. This asserts + # the contract as data: after a bare require, the umbrella file must be + # absent from $LOADED_FEATURES. + # + # Lives here rather than in snap_diff_test: its subject is the v1 + # umbrella, and once 3.0 deletes that file the grep below is empty by + # construction and the guard can never fail again. + test "bare require \"snap_diff\" never loads the umbrella capybara_screenshot_diff" do + script = <<~RUBY + require "snap_diff" + umbrella = $LOADED_FEATURES.grep(%r{/lib/capybara_screenshot_diff\\.rb\\z}) + abort("umbrella loaded via: \#{umbrella.join(", ")}") unless umbrella.empty? + RUBY + + out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script) + + assert status.success?, "expected bare `require \"snap_diff\"` to keep the umbrella unloaded, got:\n#{out}" + end + test ".start yields the same objects Diff.configure yields" do yielded = [] Capybara::Screenshot::Diff.configure { |screenshot, diff| yielded << [screenshot, diff] } diff --git a/test/unit/backtrace_filter_test.rb b/test/unit/backtrace_filter_test.rb index 540bb560..ff6a022f 100644 --- a/test/unit/backtrace_filter_test.rb +++ b/test/unit/backtrace_filter_test.rb @@ -8,7 +8,7 @@ class BacktraceFilterTest < ActiveSupport::TestCase filter = SnapDiff::BacktraceFilter.new("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/app/lib/") result = filter.filtered([ - "/app/lib/capybara_screenshot_diff/foo.rb:1:in 'bar'", + "/app/lib/snap_diff/foo.rb:1:in 'bar'", "/app/test/some_test.rb:5:in 'test_thing'" ]) @@ -48,7 +48,7 @@ class BacktraceFilterTest < ActiveSupport::TestCase test "#initialize defaults to the library's own lib directory" do filter = SnapDiff::BacktraceFilter.new - lib_file = File.expand_path("../../lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb", __dir__) + lib_file = File.expand_path("../../lib/snap_diff/error_with_filtered_backtrace.rb", __dir__) result = filter.filtered([ "#{lib_file}:1:in 'filtered'", diff --git a/test/unit/snap_diff_test.rb b/test/unit/snap_diff_test.rb index ecacd753..0d7c958a 100644 --- a/test/unit/snap_diff_test.rb +++ b/test/unit/snap_diff_test.rb @@ -65,26 +65,11 @@ class SnapDiffTest < ActiveSupport::TestCase assert status.success?, "expected bare `require \"snap_diff\"` to annotate a difference, got:\n#{out}" end - # Acyclicity contract (the #208 deadlock-class fix): the lean - # `require "snap_diff"` entry must NEVER pull the umbrella - # capybara_screenshot_diff.rb back in. The old autoload wiring had - # snap_diff <-> capybara_screenshot_diff requiring each other, which - # produced load-order deadlocks/partially-initialized constants; #208 - # broke the cycle, but until now only discipline guarded it -- a probe - # that reintroduced the cycle left the whole suite green. This asserts - # the contract as data: after a bare require, the umbrella file must be - # absent from $LOADED_FEATURES. - test "bare require \"snap_diff\" never loads the umbrella capybara_screenshot_diff" do - script = <<~RUBY - require "snap_diff" - umbrella = $LOADED_FEATURES.grep(%r{/lib/capybara_screenshot_diff\\.rb\\z}) - abort("umbrella loaded via: \#{umbrella.join(", ")}") unless umbrella.empty? - RUBY - - out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script) - - assert status.success?, "expected bare `require \"snap_diff\"` to keep the umbrella unloaded, got:\n#{out}" - end + # The acyclicity contract ("bare require never loads the umbrella") is in + # test/legacy/legacy_forwarders_test.rb: its subject is the v1 umbrella + # file, and once 3.0 deletes lib/capybara_screenshot_diff.rb the + # $LOADED_FEATURES grep is empty by construction, so the guard could never + # fail again. # Dual-install guard: both gem names ship identical files, so with BOTH # activated every require silently resolves from whichever gem activated diff --git a/test/unit/support_load_probe_test.rb b/test/unit/support_load_probe_test.rb index 78457a65..79aad042 100644 --- a/test/unit/support_load_probe_test.rb +++ b/test/unit/support_load_probe_test.rb @@ -66,8 +66,16 @@ class SupportLoadProbeTest < ActiveSupport::TestCase # SnapDiff.serve is deliberately absent here: docs/snapdiff.md's object # map gates it behind its own `require "snap_diff/static"` (which pulls # rack + minitest), so the core must not carry it. + # + # SnapDiff.start is absent for a different reason: it is defined in + # snap_diff/legacy_shims.rb and yields the two v1 config holders, so it + # cannot outlive them (#235). A CANONICAL gate demanding a method 3.0 + # deletes is a gate that fails the day the deletion lands -- which is + # exactly what it did. `.start` keeps its coverage on the legacy side: + # test/legacy/legacy_forwarders_test.rb asserts what it yields and that it + # applies a setting, and LEGACY_SESSION_SURFACE pins it per v1 entry point. CANONICAL_SURFACE = %w[ - config configure start compare session reset pending_screenshots_message + config configure compare session reset pending_screenshots_message ].freeze # snap_diff-capybara is the canonical gem's Bundler entry point, so it is