diff --git a/lib/capybara/screenshot/diff/screenshot_matcher.rb b/lib/capybara/screenshot/diff/screenshot_matcher.rb index ef075937..0e92fe21 100644 --- a/lib/capybara/screenshot/diff/screenshot_matcher.rb +++ b/lib/capybara/screenshot/diff/screenshot_matcher.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "capybara_screenshot_diff/snap_manager" require_relative "screenshoter" require_relative "stable_screenshoter" require_relative "browser_helpers" @@ -10,15 +11,14 @@ module Capybara module Screenshot module Diff class ScreenshotMatcher - attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path, :screenshot_format + attr_reader :screenshot_full_name, :driver_options, :screenshot_format def initialize(screenshot_full_name, options = {}) @screenshot_full_name = screenshot_full_name @driver_options = Diff.default_options.merge(options) @screenshot_format = @driver_options[:screenshot_format] - @screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".#{screenshot_format}") - @base_screenshot_path = ScreenshotMatcher.base_image_path_from(@screenshot_path) + @snapshot = CapybaraScreenshotDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format) end def build_screenshot_matches_job @@ -32,58 +32,49 @@ def build_screenshot_matches_job # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates # Allow nil or single or multiple areas driver_options[:skip_area] = area_calculator.calculate_skip_area - driver_options[:driver] = Drivers.for(driver_options[:driver]) - # Load base screenshot from VCS - create_output_directory_for(screenshot_path) unless screenshot_path.exist? - - checkout_base_screenshot + @snapshot.checkout_base_screenshot # When fail_if_new is true no need to create screenshot if base screenshot is missing - return if Capybara::Screenshot::Diff.fail_if_new && !base_screenshot_path.exist? - - capture_options = { - # screenshot options - capybara_screenshot_options: driver_options[:capybara_screenshot_options], - crop: driver_options.delete(:crop), - # delivery options - screenshot_format: driver_options[:screenshot_format], - # stability options - stability_time_limit: driver_options.delete(:stability_time_limit), - wait: driver_options.delete(:wait) - } + return if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist? + + capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options) # Load new screenshot from Browser - take_comparison_screenshot(capture_options, driver_options, screenshot_path) + take_comparison_screenshot(capture_options, comparison_options, @snapshot) # Pre-computation: No need to compare without base screenshot - return unless base_screenshot_path.exist? + return unless @snapshot.base_path.exist? # Add comparison job in the queue - [screenshot_full_name, ImageCompare.new(screenshot_path, base_screenshot_path, driver_options)] - end - - def self.base_image_path_from(screenshot_path) - screenshot_path.sub_ext(".base#{screenshot_path.extname}") + [screenshot_full_name, ImageCompare.new(@snapshot.path, @snapshot.base_path, comparison_options)] end private - def checkout_base_screenshot - Vcs.checkout_vcs(screenshot_path, base_screenshot_path) - end - - def create_output_directory_for(screenshot_path) - screenshot_path.dirname.mkpath + def extract_capture_and_comparison_options!(driver_options = {}) + [ + { + # screenshot options + capybara_screenshot_options: driver_options[:capybara_screenshot_options], + crop: driver_options.delete(:crop), + # delivery options + screenshot_format: driver_options[:screenshot_format], + # stability options + stability_time_limit: driver_options.delete(:stability_time_limit), + wait: driver_options.delete(:wait) + }, + driver_options + ] end # Try to get screenshot from browser. # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug - def take_comparison_screenshot(capture_options, driver_options, screenshot_path) - screenshoter = build_screenshoter_for(capture_options, driver_options) - screenshoter.take_comparison_screenshot(screenshot_path) + def take_comparison_screenshot(capture_options, comparison_options, snapshot = nil) + screenshoter = build_screenshoter_for(capture_options, comparison_options) + screenshoter.take_comparison_screenshot(snapshot) end def build_screenshoter_for(capture_options, comparison_options = {}) diff --git a/lib/capybara/screenshot/diff/screenshoter.rb b/lib/capybara/screenshot/diff/screenshoter.rb index 0042f9ff..7718746c 100644 --- a/lib/capybara/screenshot/diff/screenshoter.rb +++ b/lib/capybara/screenshot/diff/screenshoter.rb @@ -6,11 +6,10 @@ module Capybara module Screenshot class Screenshoter - attr_reader :capture_options, :comparison_options, :driver + attr_reader :capture_options, :driver def initialize(capture_options, driver) @capture_options = capture_options - @comparison_options = comparison_options @driver = driver end @@ -22,34 +21,16 @@ def wait @capture_options[:wait] end - def screenshot_format - @capture_options[:screenshot_format] || "png" - end - def capybara_screenshot_options @capture_options[:capybara_screenshot_options] || {} end - def self.attempts_screenshot_paths(base_file) - extname = Pathname.new(base_file).extname - Dir["#{base_file.to_s.chomp(extname)}.attempt_*#{extname}"].sort - end - - def self.cleanup_attempts_screenshots(base_file) - FileUtils.rm_rf attempts_screenshot_paths(base_file) - end - # Try to get screenshot from browser. # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug - def take_comparison_screenshot(screenshot_path) - capture_screenshot_at(screenshot_path) - - Screenshoter.cleanup_attempts_screenshots(screenshot_path) - end - - def self.gen_next_attempt_path(screenshot_path, iteration) - screenshot_path.sub_ext(format(".attempt_%02i#{screenshot_path.extname}", iteration)) + def take_comparison_screenshot(snapshot) + capture_screenshot_at(snapshot) + snapshot.cleanup_attempts end PNG_EXTENSION = ".png" @@ -101,6 +82,7 @@ def wait_images_loaded(timeout:) deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout loop do + Capybara.default_max_wait_time pending_image = BrowserHelpers.pending_image_to_load break unless pending_image @@ -123,18 +105,10 @@ def save_and_process_screenshot(screenshot_path) File.unlink(tmpfile) if tmpfile end - def capture_screenshot_at(screenshot_path) - new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0) - take_and_process_screenshot(new_screenshot_path, screenshot_path) - end - - def take_and_process_screenshot(new_screenshot_path, screenshot_path) - take_screenshot(new_screenshot_path) - move_screenshot_to(new_screenshot_path, screenshot_path) - end + def capture_screenshot_at(snapshot) + take_screenshot(snapshot.next_attempt_path!) - def move_screenshot_to(new_screenshot_path, screenshot_path) - FileUtils.mv(new_screenshot_path, screenshot_path, force: true) + snapshot.commit_last_attempt end def resize_if_needed(saved_image) diff --git a/lib/capybara/screenshot/diff/stable_screenshoter.rb b/lib/capybara/screenshot/diff/stable_screenshoter.rb index b8c53a54..992e9e8b 100644 --- a/lib/capybara/screenshot/diff/stable_screenshoter.rb +++ b/lib/capybara/screenshot/diff/stable_screenshoter.rb @@ -16,14 +16,14 @@ class StableScreenshoter # @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`. # @param comparison_options [Hash, nil] The options for comparing screenshots, defaults to `nil` which uses `Diff.default_options`. # @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`. - def initialize(capture_options, comparison_options = nil) - @stability_time_limit, @wait = capture_options.fetch_values(:stability_time_limit, :wait) + def initialize(capture_options, comparison_options = {}) + @stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS) raise ArgumentError, "wait should be provided for stable screenshots" unless wait raise ArgumentError, "stability_time_limit should be provided for stable screenshots" unless stability_time_limit raise ArgumentError, "stability_time_limit (#{stability_time_limit}) should be less or equal than wait (#{wait}) for stable screenshots" unless stability_time_limit <= wait - @comparison_options = comparison_options || Diff.default_options + @comparison_options = comparison_options driver = Diff::Drivers.for(@comparison_options) @screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), driver) @@ -35,92 +35,72 @@ def initialize(capture_options, comparison_options = nil) # or the `:wait` limit is reached. If unable to achieve a stable state within the time limit, it annotates the attempts # to aid debugging. # - # @param screenshot_path [String, Pathname] The path where the screenshot will be saved. + # @param snapshot Snap The snapshot details to take a stable screenshot of. # @return [void] # @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time. - def take_comparison_screenshot(screenshot_path) - new_screenshot_path = take_stable_screenshot(screenshot_path) + def take_comparison_screenshot(snapshot) + result = take_stable_screenshot(snapshot) # We failed to get stable browser state! Generate difference between attempts to overview moving parts! - unless new_screenshot_path + unless result # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally. - annotate_attempts_and_fail!(screenshot_path) + annotate_attempts_and_fail!(snapshot) end - FileUtils.mv(new_screenshot_path, screenshot_path, force: true) - Screenshoter.cleanup_attempts_screenshots(screenshot_path) + # store success attempt as actual screenshot + snapshot.commit_last_attempt + + # cleanup all previous attempts + snapshot.cleanup_attempts end - def take_stable_screenshot(screenshot_path) - screenshot_path = screenshot_path.is_a?(String) ? Pathname.new(screenshot_path) : screenshot_path + def take_stable_screenshot(snapshot) # We try to compare first attempt with checkout version, in order to not run next screenshots - attempt_path = nil deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait # Cleanup all previous attempts for sure - Screenshoter.cleanup_attempts_screenshots(screenshot_path) + snapshot.cleanup_attempts 0.step do |i| # FIXME: it should be wait, and wait should be replaced with stability_time_limit - sleep(stability_time_limit) unless i == 0 - attempt_path, prev_attempt_path = attempt_next_screenshot(attempt_path, i, screenshot_path) - return attempt_path if attempt_successful?(attempt_path, prev_attempt_path) - return nil if timeout?(deadline_at) + sleep(stability_time_limit) unless i == 0 # test prev_attempt_path is nil + + attempt_next_screenshot(snapshot) + + return true if attempt_successful?(snapshot) + return false if timeout?(deadline_at) end end private - def attempt_successful?(attempt_path, prev_attempt_path) - return false unless prev_attempt_path - build_comparison_for(attempt_path, prev_attempt_path).quick_equal? + def attempt_successful?(snapshot) + return false unless snapshot.prev_attempt_path + + build_last_attempts_comparison_for(snapshot).quick_equal? rescue ArgumentError false end - def attempt_next_screenshot(prev_attempt_path, i, screenshot_path) - new_attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i) - @screenshoter.take_screenshot(new_attempt_path) - [new_attempt_path, prev_attempt_path] + def attempt_next_screenshot(snapshot) + @screenshoter.take_screenshot(snapshot.next_attempt_path!) end def timeout?(deadline_at) Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at end - def build_comparison_for(attempt_path, previous_attempt_path) - ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options) + def build_last_attempts_comparison_for(snapshot) + ImageCompare.new(snapshot.attempt_path, snapshot.prev_attempt_path, @comparison_options) end # TODO: Move to the HistoricalReporter - def annotate_attempts_and_fail!(screenshot_path) - screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path) - - annotate_stabilization_images(screenshot_attempts) + def annotate_attempts_and_fail!(snapshot) + require "capybara_screenshot_diff/attempts_reporter" + attempts_reporter = CapybaraScreenshotDiff::AttemptsReporter.new(snapshot, @comparison_options, {wait: wait, stability_time_limit: stability_time_limit}) # TODO: Move fail to the queue after tests passed - fail("Could not get stable screenshot within #{wait}s:\n#{screenshot_attempts.join("\n")}") - end - - # TODO: Add tests that we annotate all files except first one - def annotate_stabilization_images(attempts_screenshot_paths) - previous_file = nil - attempts_screenshot_paths.reverse_each do |file_name| - if previous_file && File.exist?(previous_file) - attempts_comparison = build_comparison_for(file_name, previous_file) - - if attempts_comparison.different? - FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true) - else - warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \ - "#{previous_file} and #{file_name} are equal" - end - - FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true) - end - - previous_file = file_name - end + fail(attempts_reporter.generate) end end end diff --git a/lib/capybara/screenshot/diff/test_methods.rb b/lib/capybara/screenshot/diff/test_methods.rb index 4b162ff5..2e9d7db4 100644 --- a/lib/capybara/screenshot/diff/test_methods.rb +++ b/lib/capybara/screenshot/diff/test_methods.rb @@ -26,7 +26,7 @@ module Screenshot module Diff module TestMethods # @!attribute [rw] test_screenshots - # @return [Array(Array(Array(String), String, ImageCompare))] An array where each element is an array containing the caller context, + # @return [Array(Array(Array(String), String, ImageCompare | Minitest::Mock))] An array where each element is an array containing the caller context, # the name of the screenshot, and the comparison object. This attribute stores information about each screenshot # scheduled for comparison to ensure they do not show any unintended differences. def initialize(*) @@ -146,7 +146,7 @@ def screenshot(name, skip_stack_frames: 0, **options) # Asserts that an image has not changed compared to its baseline. # - # @param caller [Array] The caller context, used for error reporting. + # @param caller [Array(String)] The caller context, used for error reporting. # @param name [String] The name of the screenshot being verified. # @param comparison [Object] The comparison object containing the result and details of the comparison. # @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil. @@ -163,7 +163,7 @@ def assert_image_not_changed(caller, name, comparison) return unless result - "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller}" + "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller.join(", ")}" end private diff --git a/lib/capybara/screenshot/diff/vcs.rb b/lib/capybara/screenshot/diff/vcs.rb index f8672724..532f0890 100644 --- a/lib/capybara/screenshot/diff/vcs.rb +++ b/lib/capybara/screenshot/diff/vcs.rb @@ -6,21 +6,35 @@ module Capybara module Screenshot module Diff module Vcs - SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null" + def self.checkout_vcs(root, screenshot_path, checkout_path) + if svn?(root) + restore_svn_revision(screenshot_path, checkout_path) + else + restore_git_revision(screenshot_path, checkout_path, root: root) + end + end - def self.restore_git_revision(screenshot_path, checkout_path) - vcs_file_path = screenshot_path.relative_path_from(Screenshot.root) + def self.svn?(root) + (root / ".svn").exist? + end + + SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null" + def self.restore_git_revision(screenshot_path, checkout_path = screenshot_path, root:) + vcs_file_path = screenshot_path.relative_path_from(root) redirect_target = "#{checkout_path} #{SILENCE_ERRORS}" show_command = "git show HEAD~0:./#{vcs_file_path}" - if Screenshot.use_lfs - `#{show_command} > #{checkout_path}.tmp #{SILENCE_ERRORS}` - if $CHILD_STATUS == 0 - `git lfs smudge < #{checkout_path}.tmp > #{redirect_target}` + + Dir.chdir(root) do + if Screenshot.use_lfs + system("#{show_command} > #{checkout_path}.tmp #{SILENCE_ERRORS}", exception: !!ENV["DEBUG"]) + + `git lfs smudge < #{checkout_path}.tmp > #{redirect_target}` if $CHILD_STATUS == 0 + + File.delete "#{checkout_path}.tmp" + else + system("#{show_command} > #{redirect_target}", exception: !!ENV["DEBUG"]) end - File.delete "#{checkout_path}.tmp" - else - `#{show_command} > #{redirect_target}` end if $CHILD_STATUS != 0 @@ -31,14 +45,6 @@ def self.restore_git_revision(screenshot_path, checkout_path) end end - def self.checkout_vcs(screenshot_path, checkout_path) - if svn? - restore_svn_revision(screenshot_path, checkout_path) - else - restore_git_revision(screenshot_path, checkout_path) - end - end - def self.restore_svn_revision(screenshot_path, checkout_path) committed_file_name = screenshot_path + "../.svn/text-base/" + "#{screenshot_path.basename}.svn-base" if committed_file_name.exist? @@ -60,10 +66,6 @@ def self.restore_svn_revision(screenshot_path, checkout_path) false end - - def self.svn? - (Screenshot.screenshot_area_abs / ".svn").exist? - end end end end diff --git a/lib/capybara_screenshot_diff.rb b/lib/capybara_screenshot_diff.rb index 5aa1915d..3e3bd604 100644 --- a/lib/capybara_screenshot_diff.rb +++ b/lib/capybara_screenshot_diff.rb @@ -4,9 +4,9 @@ require "capybara/screenshot/diff/version" require "capybara/screenshot/diff/utils" require "capybara/screenshot/diff/image_compare" +require "capybara_screenshot_diff/snap_manager" require "capybara/screenshot/diff/test_methods" require "capybara/screenshot/diff/screenshoter" - require "capybara/screenshot/diff/reporters/default" module CapybaraScreenshotDiff @@ -63,6 +63,7 @@ module Diff mattr_accessor :tolerance mattr_accessor(:screenshoter) { Screenshoter } + mattr_accessor(:manager) { CapybaraScreenshotDiff::SnapManager } AVAILABLE_DRIVERS = Utils.detect_available_drivers.freeze diff --git a/lib/capybara_screenshot_diff/attempts_reporter.rb b/lib/capybara_screenshot_diff/attempts_reporter.rb new file mode 100644 index 00000000..08ee3f92 --- /dev/null +++ b/lib/capybara_screenshot_diff/attempts_reporter.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "capybara/screenshot/diff/image_compare" + +module CapybaraScreenshotDiff + class AttemptsReporter + def initialize(snapshot, comparison_options, stability_options = {}) + @snapshot = snapshot + @comparison_options = comparison_options + @wait = stability_options[:wait] + end + + def generate + attempts_screenshot_paths = @snapshot.find_attempts_paths + + annotate_attempts(attempts_screenshot_paths) + + "Could not get stable screenshot within #{@wait}s:\n#{attempts_screenshot_paths.join("\n")}" + end + + def build_comparison_for(attempt_path, previous_attempt_path) + Capybara::Screenshot::Diff::ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options) + end + + private + + def annotate_attempts(attempts_screenshot_paths) + previous_file = nil + attempts_screenshot_paths.reverse_each do |file_name| + if previous_file && File.exist?(previous_file) + attempts_comparison = build_comparison_for(file_name, previous_file) + + if attempts_comparison.different? + FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true) + else + warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \ + "#{previous_file} and #{file_name} are equal" + end + + FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true) + end + + previous_file = file_name + end + + previous_file + end + end +end diff --git a/lib/capybara_screenshot_diff/dsl.rb b/lib/capybara_screenshot_diff/dsl.rb index 8a58739f..188463d2 100644 --- a/lib/capybara_screenshot_diff/dsl.rb +++ b/lib/capybara_screenshot_diff/dsl.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "capybara_screenshot_diff" +require "capybara/screenshot/diff/test_methods" module CapybaraScreenshotDiff module DSL diff --git a/lib/capybara_screenshot_diff/snap.rb b/lib/capybara_screenshot_diff/snap.rb new file mode 100644 index 00000000..cbc95696 --- /dev/null +++ b/lib/capybara_screenshot_diff/snap.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module CapybaraScreenshotDiff + class Snap + attr_reader :full_name, :format, :path, :base_path, :manager, :attempt_path, :prev_attempt_path, :attempts_count + + def initialize(full_name, format, manager: SnapManager.instance) + @full_name = full_name + @format = format + @path = manager.abs_path_for(Pathname.new(@full_name).sub_ext(".#{@format}")) + @base_path = @path.sub_ext(".base.#{@format}") + @manager = manager + @attempts_count = 0 + end + + def delete! + path.delete if path.exist? + base_path.delete if base_path.exist? + cleanup_attempts + end + + def checkout_base_screenshot + @manager.checkout_file(path, base_path) + end + + def path_for(version = :actual) + case version + when :base + base_path + else + path + end + end + + def next_attempt_path! + @prev_attempt_path = @attempt_path + @attempt_path = path.sub_ext(sprintf(".attempt_%02i.#{format}", @attempts_count)) + ensure + @attempts_count += 1 + end + + def commit_last_attempt + @manager.move(attempt_path, path) + end + + def cleanup_attempts + @manager.cleanup_attempts!(self) + @attempts_count = 0 + end + + def find_attempts_paths + Dir[@manager.abs_path_for "**/#{full_name}.attempt_*.#{format}"] + end + end +end diff --git a/lib/capybara_screenshot_diff/snap_manager.rb b/lib/capybara_screenshot_diff/snap_manager.rb new file mode 100644 index 00000000..0a75c98d --- /dev/null +++ b/lib/capybara_screenshot_diff/snap_manager.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "capybara/screenshot/diff/vcs" +require "active_support/core_ext/module/attribute_accessors" + +require "capybara_screenshot_diff/snap" + +module CapybaraScreenshotDiff + class SnapManager + attr_reader :root + + def initialize(root) + @root = Pathname.new(root) + end + + def snapshot(screenshot_full_name, screenshot_format = "png") + Snap.new(screenshot_full_name, screenshot_format, manager: self) + end + + def self.snapshot(screenshot_full_name, screenshot_format = "png") + instance.snapshot(screenshot_full_name, screenshot_format) + end + + def abs_path_for(relative_path) + @root / relative_path + end + + def checkout_file(path, as_path) + create_output_directory_for(as_path) unless as_path.exist? + Capybara::Screenshot::Diff::Vcs.checkout_vcs(root, path, as_path) + end + + def provision_snap_with(snap, path, version: :actual) + managed_path = snap.path_for(version) + create_output_directory_for(managed_path) unless managed_path.exist? + FileUtils.cp(path, managed_path) + end + + def create_output_directory_for(path = nil) + path ? path.dirname.mkpath : root.mkpath + end + + # TODO: rename to delete! + def cleanup! + FileUtils.rm_rf root, secure: true + end + + def self.cleanup! + instance.cleanup! + end + + def cleanup_attempts!(snapshot) + FileUtils.rm_rf snapshot.find_attempts_paths, secure: true + end + + def move(new_screenshot_path, screenshot_path) + FileUtils.mv(new_screenshot_path, screenshot_path, force: true) + end + + def screenshots + root.children.map { |f| f.basename.to_s } + end + + def self.screenshots + instance.screenshots + end + + def self.root + instance.root + end + + def self.instance + Capybara::Screenshot::Diff.manager.new(Capybara::Screenshot.screenshot_area_abs) + end + end +end diff --git a/test/capybara/screenshot/diff/image_compare_test.rb b/test/capybara/screenshot/diff/image_compare_test.rb index 0aad8542..eac0d597 100644 --- a/test/capybara/screenshot/diff/image_compare_test.rb +++ b/test/capybara/screenshot/diff/image_compare_test.rb @@ -111,28 +111,25 @@ class IntegrationRegressionTest < ActionDispatch::IntegrationTest images = all_fixtures_images_names AVAILABLE_DRIVERS.each do |driver| - Dir.chdir File.expand_path("../../../images", __dir__) do - 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} with #{different_image} with #{driver} driver should not be quick_equal" - ) - assert( - comparison.different?, - "compare #{image} with #{different_image} with #{driver} driver should be different" - ) - end + 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 def all_fixtures_images_names - fixtures_images = Dir[File.expand_path("../../../images/*.png", __dir__)] - fixtures_images.map { |f| File.basename(f).chomp(".png") } + %w[a a_cropped b c d portrait portrait_b] end end end diff --git a/test/capybara/screenshot/diff/stable_screenshoter_test.rb b/test/capybara/screenshot/diff/stable_screenshoter_test.rb index bd17e740..d651e54a 100644 --- a/test/capybara/screenshot/diff/stable_screenshoter_test.rb +++ b/test/capybara/screenshot/diff/stable_screenshoter_test.rb @@ -8,6 +8,15 @@ module Diff class StableScreenshoterTest < ActionDispatch::IntegrationTest include TestMethodsStub + setup do + @manager = CapybaraScreenshotDiff::SnapManager.new(Capybara::Screenshot.root / "stable_screenshoter_test") + @manager.create_output_directory_for + end + + teardown do + @manager.cleanup! + end + test "#take_stable_screenshot several iterations to take stable screenshot" do image_compare_stub = build_image_compare_stub @@ -18,7 +27,8 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest mock.expect(:quick_equal?, true) ImageCompare.stub :new, mock do - take_stable_screenshot_with("tmp/02_a.png") + snap = @manager.snapshot("02_a") + take_stable_screenshot_with(snap) end mock.verify @@ -26,13 +36,13 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest test "#take_stable_screenshot without wait raises any error" do assert_raises ArgumentError, "wait should be provided" do - take_stable_screenshot_with("tmp/02_a.png", wait: nil) + take_stable_screenshot_with(@manager.snapshot("02_a"), wait: nil) end end test "#take_stable_screenshot without stability_time_limit raises any error" do assert_raises ArgumentError, "stability_time_limit should be provided" do - take_stable_screenshot_with("tmp/02_a.png", stability_time_limit: nil) + take_stable_screenshot_with(@manager.snapshot("02_a"), stability_time_limit: nil) end end @@ -43,26 +53,30 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest mock.expect(:quick_equal?, false) mock.expect(:quick_equal?, true) - assert_not (Capybara::Screenshot.root / "02_a.png").exist? + snap = @manager.snapshot("02_a") + assert_not_predicate snap.path, :exist? ImageCompare.stub :new, mock do StableScreenshoter .new({stability_time_limit: 0.5, wait: 1}, image_compare_stub.driver_options) - .take_comparison_screenshot("tmp/02_a.png") + .take_comparison_screenshot(snap) end mock.verify - assert_empty Dir[Capybara::Screenshot.root / "**/02_a.attempt_*.png"] - assert (Capybara::Screenshot.root / "02_a.png").exist? - assert_not_predicate (Capybara::Screenshot.root / "02_a.png").size, :zero? + assert_empty snap.find_attempts_paths + assert_predicate snap.path, :exist? + assert_not_predicate snap.path.size, :zero? end test "#take_comparison_screenshot fail on missing find stable image in time and generates annotated history screenshots" do - screenshot_path = Pathname.new("tmp/01_a.png") + 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 - annotated_screenshot_path = Pathname.new("tmp/02_a.png") + 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")] @@ -71,9 +85,9 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest FileUtils.touch(annotated_attempts_paths) mock = ::Minitest::Mock.new(build_image_compare_stub(equal: false)) - annotated_attempts_paths.reverse_each do |(latest_path, committed_path)| - mock.reporter.expect(:annotated_image_path, latest_path.to_s) - mock.reporter.expect(:annotated_base_image_path, committed_path.to_s) + 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 RuntimeError, "Could not get stable screenshot within 1s" do @@ -81,7 +95,7 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest # Wait time is less then stability time, which will generate problem StableScreenshoter .new({stability_time_limit: 0.5, wait: 1}, build_image_compare_stub(equal: false).driver_options) - .take_comparison_screenshot(screenshot_path.to_s) + .take_comparison_screenshot(snap) end end @@ -98,7 +112,8 @@ class StableScreenshoterTest < ActionDispatch::IntegrationTest 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 - FileUtils.rm_rf Dir["tmp/01_a*.png"] + snap&.delete! + pseudo_snap_for_annotations&.delete! end end end diff --git a/test/capybara/screenshot/diff/test_methods_test.rb b/test/capybara/screenshot/diff/test_methods_test.rb index 159d422b..ffbee4e3 100644 --- a/test/capybara/screenshot/diff/test_methods_test.rb +++ b/test/capybara/screenshot/diff/test_methods_test.rb @@ -20,7 +20,7 @@ class TestMethodsTest < ActionDispatch::IntegrationTest end def test_assert_image_not_changed - message = assert_image_not_changed("my_test.rb:42", "name", make_comparison(:a, :c)) + message = assert_image_not_changed(["my_test.rb:42"], "name", make_comparison(:a, :c)) value = (RUBY_VERSION >= "2.4") ? 187.4 : 188 assert_equal <<-MSG.strip_heredoc.chomp, message Screenshot does not match for 'name' ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value}}) @@ -33,7 +33,7 @@ def test_assert_image_not_changed def test_assert_image_not_changed_with_shift_distance_limit message = assert_image_not_changed( - "my_test.rb:42", + ["my_test.rb:42"], "name", make_comparison(:a, :c, shift_distance_limit: 1, driver: :chunky_png) ) @@ -55,7 +55,7 @@ def test_screenshot_support_drivers_options def test_skip_stack_frames Vcs.stub(:checkout_vcs, true) do assert_predicate @test_screenshots, :blank? - make_comparison(:a, :c, destination: Rails.root / "doc/screenshots/a.png") + make_comparison(:a, :c, destination: "a.png") our_screenshot("a", 1) assert_equal 1, @test_screenshots.size @@ -83,18 +83,20 @@ def test_skip_area_and_stability_time_limit def test_creates_new_screenshot screenshot(:c) - assert_predicate (Capybara::Screenshot.screenshot_area_abs / "c.png"), :exist? + + snap = CapybaraScreenshotDiff::SnapManager.snapshot("c") + assert_predicate snap.path, :exist? end def test_cleanup_base_image_for_no_change comparison = make_comparison(:a, :a) - assert_image_not_changed("my_test.rb:42", "name", comparison) + assert_image_not_changed(["my_test.rb:42"], "name", comparison) assert_not comparison.base_image_path.exist? end def test_cleanup_base_image_for_changes comparison = make_comparison(:a, :b) - assert_image_not_changed("my_test.rb:42", "name", comparison) + assert_image_not_changed(["my_test.rb:42"], "name", comparison) assert_not comparison.base_image_path.exist? end diff --git a/test/capybara/screenshot/diff/vcs_test.rb b/test/capybara/screenshot/diff/vcs_test.rb index 3982c786..0752b419 100644 --- a/test/capybara/screenshot/diff/vcs_test.rb +++ b/test/capybara/screenshot/diff/vcs_test.rb @@ -9,7 +9,7 @@ class VcsTest < ActionDispatch::IntegrationTest include Vcs setup do - @base_screenshot = Tempfile.new(%w[vcs_base_screenshot attempt.0.png], Rails.root) + @base_screenshot = Tempfile.new(%w[vcs_base_screenshot. .attempt.0.png], Screenshot.root) end teardown do @@ -20,17 +20,12 @@ class VcsTest < ActionDispatch::IntegrationTest end test "checkout of original screenshot" do - prev_root = Capybara::Screenshot.root - Capybara::Screenshot.root = "." - - screenshot_path = Capybara::Screenshot.root / "test/images/a.png" + screenshot_path = Screenshot.root / "../test/images/a.png" base_screenshot_path = Pathname.new(@base_screenshot.path) - assert Vcs.restore_git_revision(screenshot_path, base_screenshot_path) + assert Vcs.restore_git_revision(screenshot_path, base_screenshot_path, root: Screenshot.root) assert base_screenshot_path.exist? assert_equal screenshot_path.size, base_screenshot_path.size - ensure - Capybara::Screenshot.root = prev_root end end end diff --git a/test/capybara/screenshot/diff_test.rb b/test/capybara/screenshot/diff_test.rb index efb121e3..90ef430e 100644 --- a/test/capybara/screenshot/diff_test.rb +++ b/test/capybara/screenshot/diff_test.rb @@ -27,7 +27,7 @@ class DiffTest < ActionDispatch::IntegrationTest include Diff::TestMethodsStub teardown do - FileUtils.rm_rf Capybara::Screenshot.screenshot_area_abs + CapybaraScreenshotDiff::SnapManager.cleanup! Capybara::Screenshot.add_driver_path = @orig_add_driver_path Capybara::Screenshot.add_os_path = @orig_add_os_path @@ -148,7 +148,7 @@ def _test_sample_screenshot_error mock.expect(:error_message, "expected error message") @test_screenshots = [] - @test_screenshots << ["my_test.rb:42", "sample_screenshot", mock] + @test_screenshots << [["my_test.rb:42"], "sample_screenshot", mock] mock.expect(:clear_screenshots, @test_screenshots) end end @@ -176,7 +176,7 @@ def _test_sample_screenshot_error comparison.expect(:base_image_path, Pathname.new("screenshot.base.png")) comparison.expect(:error_message, "expected error message for non minitest") - @test_screenshots << ["my_test.rb:42", "sample_screenshot", comparison] + @test_screenshots << [["my_test.rb:42"], "sample_screenshot", comparison] end end @@ -195,7 +195,9 @@ class ScreenshotFormatTest < ActionDispatch::IntegrationTest test "use default screenshot format" do skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips) - set_test_images("a.webp", :a, :a) + snap = CapybaraScreenshotDiff::SnapManager.snapshot("a", "webp") + + set_test_images(snap, :a, :a) Capybara::Screenshot.stub(:screenshot_format, "webp") do screenshot "a", driver: :vips @@ -205,7 +207,8 @@ class ScreenshotFormatTest < ActionDispatch::IntegrationTest end test "override default screenshot format" do - set_test_images("a.png", :a, :a) + snap = CapybaraScreenshotDiff::SnapManager.snapshot("a", "png") + set_test_images(snap, :a, :a) Capybara::Screenshot.stub(:screenshot_format, "webp") do screenshot "a", screenshot_format: "png" diff --git a/test/capybara/screenshot/screenshot_test.rb b/test/capybara/screenshot/screenshot_test.rb index cc242037..4fb273db 100644 --- a/test/capybara/screenshot/screenshot_test.rb +++ b/test/capybara/screenshot/screenshot_test.rb @@ -6,7 +6,7 @@ module Capybara class ScreenshotTest < ActionDispatch::IntegrationTest def test_screenshot_area_abs_is_absolute - assert Capybara::Screenshot.screenshot_area_abs.absolute? + assert CapybaraScreenshotDiff::SnapManager.root.absolute? end def test_root_is_a_pathname diff --git a/test/capybara_screenshot_diff/snap_manager_test.rb b/test/capybara_screenshot_diff/snap_manager_test.rb new file mode 100644 index 00000000..c5315828 --- /dev/null +++ b/test/capybara_screenshot_diff/snap_manager_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +module CapybaraScreenshotDiff + class SnapManagerTest < ActiveSupport::TestCase + setup do + @manager = SnapManager.new(Dir.mktmpdir("snap_diff-storage")) + 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") + + @manager.provision_snap_with(snap, path) + + 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") + + @manager.provision_snap_with(snap, path, version: :base) + + assert_not_predicate snap.path, :exist? + assert_predicate snap.base_path, :exist? + end + end +end diff --git a/test/integration/browser_screenshot_test.rb b/test/integration/browser_screenshot_test.rb index e0685e28..5db08c0d 100644 --- a/test/integration/browser_screenshot_test.rb +++ b/test/integration/browser_screenshot_test.rb @@ -187,7 +187,7 @@ def test_screenshot_selected_element end end ensure - FileUtils.rm_rf(Capybara::Screenshot.screenshot_area_abs / "index-with-anim.png") + CapybaraScreenshotDiff::SnapManager.snapshot("index-with-anim").delete! end def test_await_all_images_are_loaded diff --git a/test/support/stub_test_methods.rb b/test/support/stub_test_methods.rb index ffe065ea..2498e83c 100644 --- a/test/support/stub_test_methods.rb +++ b/test/support/stub_test_methods.rb @@ -10,34 +10,28 @@ module TestMethodsStub included do setup do + @manager = CapybaraScreenshotDiff::SnapManager.new(Rails.root / "doc/screenshots") Diff.screenshoter = ScreenshoterStub end teardown do + @manager.cleanup! Diff.screenshoter = Screenshoter end end # Prepare comparison images and build ImageCompare for them - def make_comparison(fixture_base_image, fixture_new_image, destination: nil, **options) - destination ||= Rails.root / "doc/screenshots/screenshot.png" + def make_comparison(fixture_base_image, fixture_new_image, destination: "screenshot", **options) + snap = @manager.snapshot(destination) - set_test_images(destination, fixture_base_image, fixture_new_image) + set_test_images(snap, fixture_base_image, fixture_new_image) - ImageCompare.new(destination, ScreenshotMatcher.base_image_path_from(destination), **options) + ImageCompare.new(snap.path, snap.base_path, **options) end - def set_test_images(destination, original_base_image, original_new_image, ext: "png") - destination = Pathname.new(destination) unless destination.is_a?(Pathname) - destination = Capybara::Screenshot.screenshot_area_abs.join(destination) unless destination.absolute? - destination.dirname.mkpath unless destination.dirname.exist? - - ext = destination.extname[1..] if destination.extname.present? - FileUtils.cp(TEST_IMAGES_DIR / "#{original_new_image}.#{ext}", destination) - FileUtils.cp( - TEST_IMAGES_DIR / "#{original_base_image}.#{ext}", - ScreenshotMatcher.base_image_path_from(destination) - ) + def set_test_images(snap, original_base_image, original_new_image, ext: "png") + @manager.provision_snap_with(snap, fixture_image_path_from(original_new_image, snap.format), version: :actual) + @manager.provision_snap_with(snap, fixture_image_path_from(original_base_image, snap.format), version: :base) end ImageCompareStub = Struct.new( @@ -55,9 +49,9 @@ def build_image_compare_stub(equal: true) ) end - def take_stable_screenshot_with(screenshot_path, stability_time_limit: 0.01, wait: 10) + def take_stable_screenshot_with(snap, stability_time_limit: 0.01, wait: 10) screenshoter = StableScreenshoter.new({stability_time_limit: stability_time_limit, wait: wait}) - screenshoter.take_stable_screenshot(screenshot_path) + screenshoter.take_stable_screenshot(snap) end end end diff --git a/test/system_test_case.rb b/test/system_test_case.rb index 0526fbe5..5fffa4c0 100644 --- a/test/system_test_case.rb +++ b/test/system_test_case.rb @@ -61,7 +61,7 @@ def rollback_comparison_runtime_files((_, _, comparison)) save_annotations_for_debug(comparison) screenshot_path = comparison.image_path - Vcs.restore_git_revision(screenshot_path, screenshot_path) + Vcs.restore_git_revision(screenshot_path, root: Capybara::Screenshot.root) if comparison.difference comparison.reporter.clean_tmp_files diff --git a/test/test_helper.rb b/test/test_helper.rb index f61fdfee..f463fd1b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -40,18 +40,20 @@ def optional_test end end + def fixture_image_path_from(original_new_image, ext = "png") + TEST_IMAGES_DIR / "#{original_new_image}.#{ext}" + end + def assert_same_images(expected_image_name, image_path) expected_image_path = file_fixture("files/comparisons/#{expected_image_name}") assert_predicate(Capybara::Screenshot::Diff::ImageCompare.new(image_path, expected_image_path), :quick_equal?) end def assert_stored_screenshot(filename) - screenshots = Capybara::Screenshot.screenshot_area_abs.children.map { |f| f.basename.to_s } - assert_includes( - screenshots, + CapybaraScreenshotDiff::SnapManager.screenshots, filename, - "Screenshot #{filename} not found in #{Capybara::Screenshot.screenshot_area_abs}" + "Screenshot #{filename} not found in #{CapybaraScreenshotDiff::SnapManager.instance.root}" ) end end