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/fixtures/rspec_after_hook_order_masking_spec.rb b/test/fixtures/rspec_after_hook_order_masking_spec.rb index 8b876072..674acd05 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,23 +40,23 @@ 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 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 5a321c9c..0848639e 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,23 +27,23 @@ 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 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 d3781cfd..fbbc5347 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,17 +14,17 @@ 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 + 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/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/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/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..52f3b407 --- /dev/null +++ b/test/legacy/legacy_entry_point_probe_test.rb @@ -0,0 +1,158 @@ +# 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 + + # 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. + 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/legacy/legacy_forwarders_test.rb b/test/legacy/legacy_forwarders_test.rb new file mode 100644 index 00000000..b93ac092 --- /dev/null +++ b/test/legacy/legacy_forwarders_test.rb @@ -0,0 +1,124 @@ +# 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" +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 + + # 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] } + + 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/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/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..c2fb19fc 100644 --- a/test/unit/attempts_reporter_test.rb +++ b/test/unit/attempts_reporter_test.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true require "test_helper" -require "capybara_screenshot_diff" +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). @@ -13,7 +16,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 +63,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..ff6a022f 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/snap_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/snap_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..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,52 +56,45 @@ 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, 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 @@ -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/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..bbafabe1 100644 --- a/test/unit/diff_test.rb +++ b/test/unit/diff_test.rb @@ -4,249 +4,241 @@ 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 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 SnapDiff::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 SnapDiff::Minitest::Assertions - 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 + # 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") - class ScreenshotFormatTest < ActiveSupport::TestCase - setup do - @orig_screenshot_format = Capybara::Screenshot.screenshot_format - end + assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") + assertion.caller = ["my_test.rb:42"] + assertion.compare = mock + SnapDiff.session.add_assertion(assertion) - include Capybara::Screenshot::Diff - include CapybaraScreenshotDiff::DSLStub - include CapybaraScreenshotDiff::Minitest::Assertions + assert true + end + end - teardown do - Capybara::Screenshot.screenshot_format = @orig_screenshot_format - end + class SampleNotMiniTestCase + def self.setup + # noop + 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") + def self.teardown(&block) + @@teardown_callback = block + end - set_test_images(snap, :a, :a) + def teardown + instance_eval(&@@teardown_callback) if @@teardown_callback + ensure + @@teardown_callback = nil + SnapDiff.reset + end - SnapDiff.config.stub(:screenshot_format, "webp") do - screenshot "a", driver: :vips + include NonMinitest::Assertions - assert_stored_screenshot("a.webp") - end - end + 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") - test "stores screenshot using overridden format extension" do - snap = SnapDiff::SnapManager.snapshot("a", "png") - set_test_images(snap, :a, :a) + assertion = SnapDiff::ScreenshotAssertion.new("sample_screenshot") + assertion.caller = ["my_test.rb:42"] + assertion.compare = comparison + SnapDiff.session.add_assertion(assertion) + end + end - SnapDiff.config.stub(:screenshot_format, "webp") do - screenshot "a", screenshot_format: "png" + class ScreenshotFormatTest < ActiveSupport::TestCase + setup do + @orig_screenshot_format = SnapDiff.config.screenshot_format + end - assert_stored_screenshot("a.png") - end - end - end + include DSLStub + include SnapDiff::Minitest::Assertions - def screenshot_dir - File.join(Capybara::Screenshot.screenshot_area, *screenshot_namer.directory_parts) - end + teardown do + SnapDiff.config.screenshot_format = @orig_screenshot_format + 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") + + set_test_images(snap, :a, :a) + + SnapDiff.config.stub(:screenshot_format, "webp") do + screenshot "a", driver: :vips - def screenshot_namer - CapybaraScreenshotDiff.screenshot_namer + assert_stored_screenshot("a.webp") end + end + + test "stores screenshot using overridden format extension" do + snap = SnapDiff::SnapManager.snapshot("a", "png") + set_test_images(snap, :a, :a) + + 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..30178337 100644 --- a/test/unit/drivers/chunky_png_driver_test.rb +++ b/test/unit/drivers/chunky_png_driver_test.rb @@ -1,8 +1,8 @@ # 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 @@ -11,7 +11,7 @@ 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..abf22f53 100644 --- a/test/unit/drivers/vips_driver_test.rb +++ b/test/unit/drivers/vips_driver_test.rb @@ -3,7 +3,7 @@ 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 @@ -11,7 +11,7 @@ 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..a6f0bae5 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 @@ -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/dsl_test.rb b/test/unit/dsl_test.rb index cdbcf37a..39015f35 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 SnapDiff.session, :new_screenshots_present? - CapybaraScreenshotDiff.reset + SnapDiff.reset - assert_not_predicate CapybaraScreenshotDiff, :new_screenshots_present? - assert_empty CapybaraScreenshotDiff.new_screenshots - end + assert_not_predicate SnapDiff.session, :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/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 diff --git a/test/unit/image_compare_test.rb b/test/unit/image_compare_test.rb index cd4be258..f759c9f2 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 + # 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 - 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..07010acd 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 + SnapDiff::Reporting.notify(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..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 = 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. + # 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 = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.enabled + original_screenshot = config.screenshot_enabled + original_diff = 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, 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 - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.enabled = original_diff + config.screenshot_enabled = original_screenshot + 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 - # 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 = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.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, !!Capybara::Screenshot.active?, "Capybara::Screenshot.active? with #{context}" end ensure - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.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 = Capybara::Screenshot.root + original = config.root begin config.root = "/tmp" - assert_equal Pathname("/tmp"), Capybara::Screenshot.root assert_equal Pathname("/tmp"), config.root ensure - Capybara::Screenshot.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 = Capybara::Screenshot::Diff.tolerance + original = config.tolerance begin SnapDiff.configure { |c| c.tolerance = 0.0321 } - assert_equal 0.0321, Capybara::Screenshot::Diff.tolerance + assert_equal 0.0321, config.tolerance ensure - Capybara::Screenshot::Diff.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/snap_diff_test.rb b/test/unit/snap_diff_test.rb index 5f3bcf65..0d7c958a 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 @@ -74,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 @@ -114,26 +90,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..79aad042 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 -# Capybara::Screenshot::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,113 +56,45 @@ 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 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 - # --- 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 - # CapybaraScreenshotDiff.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 # 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 + # 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) @@ -171,9 +109,47 @@ 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| - 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 +170,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 +182,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) 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