diff --git a/lib/snap_diff/capture/viewport.rb b/lib/snap_diff/capture/viewport.rb new file mode 100644 index 00000000..c8449ad0 --- /dev/null +++ b/lib/snap_diff/capture/viewport.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "../browser_helpers" + +module SnapDiff + module Capture + # Per-capture viewport preparation seam. + # + # Called exactly once per capture, before the screenshoter runs and outside + # any stability retry loop. Today it only validates the window size + # (raise-only, never resizes) — the same guard ScreenshotMatcher carried + # inline before. v3 hangs scroll-position preservation and element-anchored + # capture off this seam via the +anchor+ parameter without touching callers. + module Viewport + module_function + + # @param expected_window_size [Array(Integer, Integer), nil] the configured window size + # @param anchor [Object, nil] reserved for v3 (scroll preservation / + # element-anchored capture); accepted but unused today. + # @raise [CapybaraScreenshotDiff::WindowSizeMismatchError] if the browser + # window does not match the expected size. + def prepare!(expected_window_size, anchor: nil) + return unless BrowserHelpers.window_size_is_wrong?(expected_window_size) + + current_size = BrowserHelpers.selenium? ? + BrowserHelpers.session.driver.browser.manage.window.size.to_s : + "unknown" + + raise CapybaraScreenshotDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller) + Window size mismatch detected! + Expected: #{expected_window_size.inspect} + Actual: #{current_size} + + Screenshots cannot be compared when window sizes don't match. + Please ensure the browser window is properly sized before taking screenshots. + ERROR + end + end + end +end diff --git a/lib/snap_diff/screenshot_matcher.rb b/lib/snap_diff/screenshot_matcher.rb index 1d29ac6e..5e46b37f 100644 --- a/lib/snap_diff/screenshot_matcher.rb +++ b/lib/snap_diff/screenshot_matcher.rb @@ -4,6 +4,7 @@ require_relative "screenshoter" require_relative "stable_screenshoter" require_relative "browser_helpers" +require_relative "capture/viewport" require_relative "vcs" require_relative "area_calculator" @@ -20,7 +21,7 @@ def initialize(screenshot_full_name, options = {}) end def build_screenshot_assertion(skip_stack_frames: 0) - check_window_size! + Capture::Viewport.prepare!(Capybara::Screenshot.window_size, anchor: nil) prepare_screenshot_options check_base_screenshot @@ -40,7 +41,7 @@ def build_screenshot_assertion(skip_stack_frames: 0) # Captures a screenshot without comparing it to a baseline. def capture - check_window_size! + Capture::Viewport.prepare!(Capybara::Screenshot.window_size, anchor: nil) prepare_screenshot_options capture_options, comparison_options = extract_capture_and_comparison_options(driver_options) @@ -55,23 +56,6 @@ def need_to_compare? @snapshot.base_path.exist? end - def check_window_size! - if BrowserHelpers.window_size_is_wrong?(Capybara::Screenshot.window_size) - current_size = BrowserHelpers.selenium? ? - BrowserHelpers.session.driver.browser.manage.window.size.to_s : - "unknown" - - raise CapybaraScreenshotDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller) - Window size mismatch detected! - Expected: #{Capybara::Screenshot.window_size.inspect} - Actual: #{current_size} - - Screenshots cannot be compared when window sizes don't match. - Please ensure the browser window is properly sized before taking screenshots. - ERROR - end - end - def prepare_screenshot_options area_calculator = AreaCalculator.new(driver_options.delete(:crop), driver_options[:skip_area]) diff --git a/test/unit/capture/viewport_test.rb b/test/unit/capture/viewport_test.rb new file mode 100644 index 00000000..e8a04ef2 --- /dev/null +++ b/test/unit/capture/viewport_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" +require "capybara_screenshot_diff" + +module SnapDiff + module Capture + # Direct coverage for the per-capture viewport preparation seam + # (5.5-lite item 6): raise-only window-size guard, plus the anchor: + # parameter reserved for v3 scroll preservation. + class ViewportTest < ActiveSupport::TestCase + test "prepare! is a no-op when the window size matches" do + BrowserHelpers.stub(:window_size_is_wrong?, false) do + assert_nil Viewport.prepare!([800, 600]) + end + end + + 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 + Viewport.prepare!([800, 600]) + end + assert_includes error.message, "[800, 600]" + end + end + end + + test "prepare! accepts the v3 anchor seam parameter without acting on it" do + BrowserHelpers.stub(:window_size_is_wrong?, false) do + assert_nil Viewport.prepare!([800, 600], anchor: "#some-element") + end + end + end + end +end diff --git a/test/unit/screenshot_matcher_test.rb b/test/unit/screenshot_matcher_test.rb index 4b1e9525..8611118e 100644 --- a/test/unit/screenshot_matcher_test.rb +++ b/test/unit/screenshot_matcher_test.rb @@ -153,6 +153,54 @@ def fake_stable.take_comparison_screenshot(snapshot) 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 + + # 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 + 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" + 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 + Vcs.stub(:checkout_vcs, true) do + SnapDiff::StableScreenshoter.stub(:new, ->(*, **) { fake_stable }) do + snap = create_snapshot_for(:a, :c) + 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,