capybara-screenshot-diff 1.8.0 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +1 -11
- data/capybara-screenshot-diff.gemspec +3 -4
- data/gems.rb +11 -8
- data/lib/capybara/screenshot/diff/area_calculator.rb +56 -0
- data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -5
- data/lib/capybara/screenshot/diff/comparison.rb +6 -0
- data/lib/capybara/screenshot/diff/cucumber.rb +1 -9
- data/lib/capybara/screenshot/diff/difference.rb +8 -4
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +0 -5
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +10 -5
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +15 -4
- data/lib/capybara/screenshot/diff/drivers.rb +1 -1
- data/lib/capybara/screenshot/diff/image_compare.rb +84 -114
- data/lib/capybara/screenshot/diff/region.rb +28 -7
- data/lib/capybara/screenshot/diff/reporters/default.rb +121 -0
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +36 -74
- data/lib/capybara/screenshot/diff/screenshoter.rb +47 -56
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +65 -63
- data/lib/capybara/screenshot/diff/test_methods.rb +81 -13
- data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +2 -7
- data/lib/capybara/screenshot/diff/vcs.rb +26 -20
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/lib/capybara/screenshot/diff.rb +1 -111
- data/lib/capybara-screenshot-diff.rb +1 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +49 -0
- data/lib/capybara_screenshot_diff/cucumber.rb +12 -0
- data/lib/capybara_screenshot_diff/dsl.rb +11 -0
- data/lib/capybara_screenshot_diff/minitest.rb +49 -0
- data/lib/capybara_screenshot_diff/rspec.rb +32 -0
- data/lib/capybara_screenshot_diff/snap.rb +55 -0
- data/lib/capybara_screenshot_diff/snap_manager.rb +76 -0
- data/lib/capybara_screenshot_diff.rb +86 -0
- metadata +20 -48
- data/lib/capybara/screenshot/diff/stabilization.rb +0 -0
- data/sig/capybara/screenshot/diff/diff.rbs +0 -28
- data/sig/capybara/screenshot/diff/difference.rbs +0 -33
- data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +0 -63
- data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +0 -36
- data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +0 -89
- data/sig/capybara/screenshot/diff/drivers/utils.rbs +0 -13
- data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +0 -25
- data/sig/capybara/screenshot/diff/image_compare.rbs +0 -93
- data/sig/capybara/screenshot/diff/os.rbs +0 -11
- data/sig/capybara/screenshot/diff/region.rbs +0 -43
- data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +0 -60
- data/sig/capybara/screenshot/diff/screenshoter.rbs +0 -48
- data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +0 -29
- data/sig/capybara/screenshot/diff/test_methods.rbs +0 -39
- data/sig/capybara/screenshot/diff/vcs.rbs +0 -17
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ffbb0b50c61fc1dcab03db91256897bed64caa09d65d37ee39765aec52e3d6fb
         | 
| 4 | 
            +
              data.tar.gz: 1c762e2084bf2629eae25fb5e38792ce2232bdd62edbdbf1bff5381c074b3edd
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 97356f772b607b1ff67b146d1726baabbef696b362187c5aa5833657a4a443e8a6e8a30dedec49a07bda4d115e2a7a5c8482e4ba64a958072d1aaf6cd9dab8a3
         | 
| 7 | 
            +
              data.tar.gz: '05880832a40006772c5fd72b28a4ebe9c28ca36ef49e70f132cab55acf1b97135bcd86bdb4d1b19893961fb344b0268d1c5d708354412c9c99efad03d3da85ab'
         | 
    
        data/Rakefile
    CHANGED
    
    | @@ -17,23 +17,13 @@ Rake::TestTask.new("test:integration") do |t| | |
| 17 17 | 
             
              t.test_files = FileList["test/integration/**/*_test.rb"]
         | 
| 18 18 | 
             
            end
         | 
| 19 19 |  | 
| 20 | 
            -
            Rake::TestTask.new("test:signatures") do |t|
         | 
| 21 | 
            -
              ENV["RBS_TEST_DOUBLE_SUITE"] ||= "minitest"
         | 
| 22 | 
            -
              ENV["RBS_TEST_TARGET"] ||= "Capybara::Screenshot::Diff::*"
         | 
| 23 | 
            -
              ENV["RBS_TEST_OPT"] ||= "-rset -rpathname -Isig"
         | 
| 24 | 
            -
             | 
| 25 | 
            -
              t.libs << "test"
         | 
| 26 | 
            -
              t.ruby_opts << "-r rbs/test/setup"
         | 
| 27 | 
            -
              t.test_files = FileList["test/**/*_test.rb"]
         | 
| 28 | 
            -
            end
         | 
| 29 | 
            -
             | 
| 30 20 | 
             
            task "clobber" do
         | 
| 31 21 | 
             
              puts "Cleanup tmp/*.png"
         | 
| 32 22 | 
             
              FileUtils.rm_rf(Dir["./tmp/*"])
         | 
| 33 23 | 
             
            end
         | 
| 34 24 |  | 
| 35 25 | 
             
            task "test:benchmark" do
         | 
| 36 | 
            -
              require_relative " | 
| 26 | 
            +
              require_relative "scripts/benchmark/find_region_benchmark"
         | 
| 37 27 | 
             
              benchmark = Capybara::Screenshot::Diff::Drivers::FindRegionBenchmark.new
         | 
| 38 28 |  | 
| 39 29 | 
             
              puts "For Medium Screen Size: 800x600"
         | 
| @@ -12,18 +12,17 @@ Gem::Specification.new do |spec| | |
| 12 12 | 
             
              spec.summary = "Track your GUI changes with diff assertions"
         | 
| 13 13 | 
             
              spec.description = "Save screen shots and track changes with graphical diff"
         | 
| 14 14 | 
             
              spec.homepage = "https://github.com/donv/capybara-screenshot-diff"
         | 
| 15 | 
            -
              spec.required_ruby_version = ">= 3. | 
| 15 | 
            +
              spec.required_ruby_version = ">= 3.1"
         | 
| 16 16 | 
             
              spec.license = "MIT"
         | 
| 17 17 | 
             
              spec.metadata["allowed_push_host"] = "https://rubygems.org/"
         | 
| 18 18 | 
             
              spec.files = `git ls-files -z`.split("\x0").reject do |f|
         | 
| 19 | 
            -
                f.match(%r{(^(\.|tmp|bin|test|spec|features|gemfiles|scripts)/)|(^(\.|Dockerfile|CONTRIBUTING|README))})
         | 
| 19 | 
            +
                f.match(%r{(^(\.|tmp|bin|test|spec|features|gemfiles|scripts|foo)/)|(^(\.|Dockerfile|CONTRIBUTING|README))})
         | 
| 20 20 | 
             
              end
         | 
| 21 21 |  | 
| 22 22 | 
             
              spec.bindir = "exe"
         | 
| 23 23 | 
             
              spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 24 24 | 
             
              spec.require_paths = ["lib"]
         | 
| 25 25 |  | 
| 26 | 
            -
              spec.add_runtime_dependency "actionpack", ">=  | 
| 26 | 
            +
              spec.add_runtime_dependency "actionpack", ">= 7.0", "< 9"
         | 
| 27 27 | 
             
              spec.add_runtime_dependency "capybara", ">= 2", "< 4"
         | 
| 28 | 
            -
              spec.add_runtime_dependency "chunky_png", ">= 1.3", "< 2"
         | 
| 29 28 | 
             
            end
         | 
    
        data/gems.rb
    CHANGED
    
    | @@ -8,13 +8,18 @@ gemspec path: __dir__ | |
| 8 8 | 
             
            gem "rake"
         | 
| 9 9 |  | 
| 10 10 | 
             
            # Image processing libraries
         | 
| 11 | 
            -
            gem " | 
| 11 | 
            +
            gem "chunky_png", ">= 1.3", require: false
         | 
| 12 | 
            +
            gem "oily_png", platform: :ruby, git: "https://github.com/wvanbergen/oily_png", ref: "44042006e79efd42ce4b52c1d78a4c70f0b4b1b2"
         | 
| 12 13 | 
             
            gem "ruby-vips", require: false
         | 
| 13 14 |  | 
| 14 | 
            -
             | 
| 15 | 
            -
            gem " | 
| 16 | 
            -
            gem " | 
| 17 | 
            -
            gem " | 
| 15 | 
            +
            group :test do
         | 
| 16 | 
            +
              gem "capybara", ">= 3.26"
         | 
| 17 | 
            +
              gem "mutex_m" # Needed for RubyMine debugging.  Try removing it.
         | 
| 18 | 
            +
              gem "minitest", require: false
         | 
| 19 | 
            +
              gem "minitest-stub-const", require: false
         | 
| 20 | 
            +
              gem "simplecov", require: false
         | 
| 21 | 
            +
              gem "rspec", require: false
         | 
| 22 | 
            +
            end
         | 
| 18 23 |  | 
| 19 24 | 
             
            # Capybara Server
         | 
| 20 25 | 
             
            gem "puma", require: false
         | 
| @@ -22,8 +27,7 @@ gem "rackup", require: false | |
| 22 27 |  | 
| 23 28 | 
             
            # Capybara Drivers
         | 
| 24 29 | 
             
            gem "cuprite", require: false
         | 
| 25 | 
            -
            gem "selenium-webdriver", require: false
         | 
| 26 | 
            -
            gem "webdrivers", "~> 5.0", require: false
         | 
| 30 | 
            +
            gem "selenium-webdriver", ">= 4.11", require: false
         | 
| 27 31 |  | 
| 28 32 | 
             
            # Test Frameworks
         | 
| 29 33 | 
             
            # gem "cucumber", require: false
         | 
| @@ -31,5 +35,4 @@ gem "webdrivers", "~> 5.0", require: false | |
| 31 35 |  | 
| 32 36 | 
             
            group :tools do
         | 
| 33 37 | 
             
              gem "standard", require: false
         | 
| 34 | 
            -
              gem "rbs", require: false, platform: :ruby
         | 
| 35 38 | 
             
            end
         | 
| @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Capybara
         | 
| 4 | 
            +
              module Screenshot
         | 
| 5 | 
            +
                module Diff
         | 
| 6 | 
            +
                  class AreaCalculator
         | 
| 7 | 
            +
                    def initialize(crop_coordinates, skip_area)
         | 
| 8 | 
            +
                      @crop_coordinates = crop_coordinates
         | 
| 9 | 
            +
                      @skip_area = skip_area
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    def calculate_crop
         | 
| 13 | 
            +
                      return @_calculated_crop if defined?(@_calculated_crop)
         | 
| 14 | 
            +
                      return @_calculated_crop = nil unless @crop_coordinates
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      # TODO: Move out from this class, this should be done on before screenshot and should not depend on Browser
         | 
| 17 | 
            +
                      @crop_coordinates = BrowserHelpers.bounds_for_css(@crop_coordinates).first if @crop_coordinates.is_a?(String)
         | 
| 18 | 
            +
                      @_calculated_crop = Region.from_edge_coordinates(*@crop_coordinates)
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    # Cast skip areas params into Region
         | 
| 22 | 
            +
                    # and if there is crop then makes absolute coordinates to eb relative to crop top left corner
         | 
| 23 | 
            +
                    def calculate_skip_area
         | 
| 24 | 
            +
                      return nil unless @skip_area
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      crop_region = calculate_crop
         | 
| 27 | 
            +
                      skip_area = Array(@skip_area)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                      css_selectors, coords_list = skip_area.compact.partition { |region| region.is_a? String }
         | 
| 30 | 
            +
                      regions, coords_list = coords_list.partition { |region| region.is_a? Region }
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      regions.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors))) unless css_selectors.empty?
         | 
| 33 | 
            +
                      regions.concat(build_regions_for(coords_list.flatten.each_slice(4))) unless coords_list.empty?
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      regions.compact!
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      if crop_region
         | 
| 38 | 
            +
                        regions
         | 
| 39 | 
            +
                          .map! { |region| crop_region.find_relative_intersect(region) }
         | 
| 40 | 
            +
                          .filter! { |region| region&.present? }
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                      regions
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    private
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    def build_regions_for(coordinates)
         | 
| 49 | 
            +
                      coordinates
         | 
| 50 | 
            +
                        .map { |entry| Region.from_edge_coordinates(*entry) }
         | 
| 51 | 
            +
                        .tap { |it| it.compact! }
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
            end
         | 
| @@ -30,13 +30,13 @@ module Capybara | |
| 30 30 |  | 
| 31 31 | 
             
                  IMAGE_WAIT_SCRIPT = <<~JS
         | 
| 32 32 | 
             
                    function pending_image() {
         | 
| 33 | 
            -
                       | 
| 33 | 
            +
                      const images = document.images
         | 
| 34 34 | 
             
                      for (var i = 0; i < images.length; i++) {
         | 
| 35 | 
            -
                        if (!images[i].complete) {
         | 
| 36 | 
            -
                            return images[i].src | 
| 35 | 
            +
                        if (!images[i].complete && images[i].loading !== "lazy") {
         | 
| 36 | 
            +
                            return images[i].src
         | 
| 37 37 | 
             
                        }
         | 
| 38 38 | 
             
                      }
         | 
| 39 | 
            -
                      return false | 
| 39 | 
            +
                      return false
         | 
| 40 40 | 
             
                    }(window)
         | 
| 41 41 | 
             
                  JS
         | 
| 42 42 |  | 
| @@ -83,7 +83,7 @@ module Capybara | |
| 83 83 | 
             
                  end
         | 
| 84 84 |  | 
| 85 85 | 
             
                  def self.region_for(element)
         | 
| 86 | 
            -
                    element.evaluate_script(GET_BOUNDING_CLIENT_RECT_SCRIPT).map { |point| point.negative? ? 0 : point.to_i }
         | 
| 86 | 
            +
                    element.evaluate_script(GET_BOUNDING_CLIENT_RECT_SCRIPT).map { |point| point.negative? ? 0 : point.ceil.to_i }
         | 
| 87 87 | 
             
                  end
         | 
| 88 88 |  | 
| 89 89 | 
             
                  def self.session
         | 
| @@ -1,11 +1,3 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require " | 
| 4 | 
            -
            require "capybara/screenshot/diff/test_methods"
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            World(Capybara::Screenshot::Diff::TestMethods)
         | 
| 7 | 
            -
             | 
| 8 | 
            -
            Before do
         | 
| 9 | 
            -
              Capybara::Screenshot::Diff.delayed = false
         | 
| 10 | 
            -
              Capybara::Screenshot::BrowserHelpers.resize_to(Capybara::Screenshot.window_size) if Capybara::Screenshot.window_size
         | 
| 11 | 
            -
            end
         | 
| 3 | 
            +
            require "capybara_screenshot_diff/cucumber"
         | 
| @@ -5,13 +5,17 @@ require "json" | |
| 5 5 | 
             
            module Capybara
         | 
| 6 6 | 
             
              module Screenshot
         | 
| 7 7 | 
             
                module Diff
         | 
| 8 | 
            -
                  class Difference < Struct.new(:region, :meta, :comparison)
         | 
| 8 | 
            +
                  class Difference < Struct.new(:region, :meta, :comparison, :failed_by)
         | 
| 9 9 | 
             
                    def different?
         | 
| 10 | 
            -
                      !(blank? || tolerable?)
         | 
| 10 | 
            +
                      failed? || !(blank? || tolerable?)
         | 
| 11 11 | 
             
                    end
         | 
| 12 12 |  | 
| 13 | 
            -
                    def  | 
| 14 | 
            -
                       | 
| 13 | 
            +
                    def equal?
         | 
| 14 | 
            +
                      !different?
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def failed?
         | 
| 18 | 
            +
                      !!failed_by
         | 
| 15 19 | 
             
                    end
         | 
| 16 20 |  | 
| 17 21 | 
             
                    def options
         | 
| @@ -1,6 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require "chunky_png"
         | 
| 4 3 | 
             
            require "capybara/screenshot/diff/difference"
         | 
| 5 4 |  | 
| 6 5 | 
             
            module Capybara
         | 
| @@ -31,10 +30,6 @@ module Capybara | |
| 31 30 | 
             
                      def dimension(image)
         | 
| 32 31 | 
             
                        [width_for(image), height_for(image)]
         | 
| 33 32 | 
             
                      end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                      def inscribed?(dimensions, i)
         | 
| 36 | 
            -
                        width_for(i) < dimensions[0] || height_for(i) < dimensions[1]
         | 
| 37 | 
            -
                      end
         | 
| 38 33 | 
             
                    end
         | 
| 39 34 | 
             
                  end
         | 
| 40 35 | 
             
                end
         | 
| @@ -1,6 +1,11 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 3 | 
            +
            begin
         | 
| 4 | 
            +
              require "chunky_png"
         | 
| 5 | 
            +
            rescue LoadError => e
         | 
| 6 | 
            +
              raise 'Required chunky_png gem is missing. Add `gem "chunky_png"` to Gemfile' if e.message.match?(/chunky_png/i)
         | 
| 7 | 
            +
              raise
         | 
| 8 | 
            +
            end
         | 
| 4 9 |  | 
| 5 10 | 
             
            require "capybara/screenshot/diff/drivers/base_driver"
         | 
| 6 11 |  | 
| @@ -35,12 +40,12 @@ module Capybara | |
| 35 40 | 
             
                        i.crop(*region.to_top_left_corner_coordinates)
         | 
| 36 41 | 
             
                      end
         | 
| 37 42 |  | 
| 38 | 
            -
                      def from_file( | 
| 39 | 
            -
                        ChunkyPNG::Image.from_file( | 
| 43 | 
            +
                      def from_file(filename_or_path)
         | 
| 44 | 
            +
                        ChunkyPNG::Image.from_file(filename_or_path.to_s)
         | 
| 40 45 | 
             
                      end
         | 
| 41 46 |  | 
| 42 47 | 
             
                      def save_image_to(image, filename)
         | 
| 43 | 
            -
                        image.save(filename)
         | 
| 48 | 
            +
                        image.save(filename, :fast_rgba)
         | 
| 44 49 | 
             
                      end
         | 
| 45 50 |  | 
| 46 51 | 
             
                      def resize_image_to(image, new_width, new_height)
         | 
| @@ -48,7 +53,7 @@ module Capybara | |
| 48 53 | 
             
                      end
         | 
| 49 54 |  | 
| 50 55 | 
             
                      def load_image_files(old_file_name, file_name)
         | 
| 51 | 
            -
                        [ | 
| 56 | 
            +
                        [old_file_name.binread, file_name.binread]
         | 
| 52 57 | 
             
                      end
         | 
| 53 58 |  | 
| 54 59 | 
             
                      def draw_rectangles(images, region, (r, g, b), offset: 0)
         | 
| @@ -3,7 +3,7 @@ | |
| 3 3 | 
             
            begin
         | 
| 4 4 | 
             
              require "vips"
         | 
| 5 5 | 
             
            rescue LoadError => e
         | 
| 6 | 
            -
               | 
| 6 | 
            +
              raise 'Required ruby-vips gem is missing. Add `gem "ruby-vips"` to Gemfile' if e.message.match?(/vips/i)
         | 
| 7 7 | 
             
              raise
         | 
| 8 8 | 
             
            end
         | 
| 9 9 |  | 
| @@ -15,6 +15,8 @@ module Capybara | |
| 15 15 | 
             
                  # Compare two images and determine if they are equal, different, or within some comparison
         | 
| 16 16 | 
             
                  # range considering color values and difference area size.
         | 
| 17 17 | 
             
                  module Drivers
         | 
| 18 | 
            +
                    DEFAULT_HIGHLIGHT_COLOR = [255, 0, 0, 255].freeze
         | 
| 19 | 
            +
             | 
| 18 20 | 
             
                    class VipsDriver < BaseDriver
         | 
| 19 21 | 
             
                      def find_difference_region(comparison)
         | 
| 20 22 | 
             
                        new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options
         | 
| @@ -26,9 +28,8 @@ module Capybara | |
| 26 28 | 
             
                        result = Difference.new(region, {}, comparison)
         | 
| 27 29 |  | 
| 28 30 | 
             
                        unless result.blank?
         | 
| 29 | 
            -
                          meta =  | 
| 30 | 
            -
                          meta[: | 
| 31 | 
            -
                          result.meta = meta
         | 
| 31 | 
            +
                          result.meta[:difference_level] = difference_level(diff_mask, base_image) if comparison.options[:tolerance]
         | 
| 32 | 
            +
                          result.meta[:diff_mask] = diff_mask
         | 
| 32 33 | 
             
                        end
         | 
| 33 34 |  | 
| 34 35 | 
             
                        result
         | 
| @@ -49,6 +50,8 @@ module Capybara | |
| 49 50 | 
             
                      end
         | 
| 50 51 |  | 
| 51 52 | 
             
                      def add_black_box(memo, region)
         | 
| 53 | 
            +
                        return memo unless region
         | 
| 54 | 
            +
             | 
| 52 55 | 
             
                        memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
         | 
| 53 56 | 
             
                      end
         | 
| 54 57 |  | 
| @@ -100,6 +103,14 @@ module Capybara | |
| 100 103 | 
             
                        (comparison.new_image == comparison.base_image).min == 255
         | 
| 101 104 | 
             
                      end
         | 
| 102 105 |  | 
| 106 | 
            +
                      def merge(new_image, base_image)
         | 
| 107 | 
            +
                        base_image.composite2(new_image, :over)
         | 
| 108 | 
            +
                      end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                      def highlight_mask(diff_mask, merged_image, color: DEFAULT_HIGHLIGHT_COLOR)
         | 
| 111 | 
            +
                        diff_mask.ifthenelse(color, merged_image * 0.75)
         | 
| 112 | 
            +
                      end
         | 
| 113 | 
            +
             | 
| 103 114 | 
             
                      private
         | 
| 104 115 |  | 
| 105 116 | 
             
                      def same_as?(region, base_image)
         | 
| @@ -5,7 +5,7 @@ module Capybara | |
| 5 5 | 
             
                module Diff
         | 
| 6 6 | 
             
                  module Drivers
         | 
| 7 7 | 
             
                    def self.for(driver_options = {})
         | 
| 8 | 
            -
                      driver_option = driver_options.fetch(:driver, :chunky_png)
         | 
| 8 | 
            +
                      driver_option = driver_options.is_a?(Hash) ? driver_options.fetch(:driver, :chunky_png) : driver_options
         | 
| 9 9 | 
             
                      return driver_option unless driver_option.is_a?(Symbol)
         | 
| 10 10 |  | 
| 11 11 | 
             
                      Utils.find_driver_class_for(driver_option).new
         | 
| @@ -1,5 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "capybara/screenshot/diff/comparison"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            module Capybara
         | 
| 4 6 | 
             
              module Screenshot
         | 
| 5 7 | 
             
                module Diff
         | 
| @@ -11,22 +13,13 @@ module Capybara | |
| 11 13 | 
             
                    TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
         | 
| 12 14 |  | 
| 13 15 | 
             
                    attr_reader :driver, :driver_options
         | 
| 14 | 
            -
             | 
| 15 | 
            -
                    attr_reader : | 
| 16 | 
            -
                      :image_path, :base_image_path,
         | 
| 17 | 
            -
                      :new_file_name, :old_file_name
         | 
| 16 | 
            +
                    attr_reader :image_path, :base_image_path
         | 
| 17 | 
            +
                    attr_reader :difference, :error_message
         | 
| 18 18 |  | 
| 19 19 | 
             
                    def initialize(image_path, base_image_path, options = {})
         | 
| 20 20 | 
             
                      @image_path = Pathname.new(image_path)
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                      @new_file_name = @image_path.to_s
         | 
| 23 | 
            -
                      @annotated_image_path = @image_path.sub_ext(".diff.png")
         | 
| 24 | 
            -
             | 
| 25 21 | 
             
                      @base_image_path = Pathname.new(base_image_path)
         | 
| 26 22 |  | 
| 27 | 
            -
                      @old_file_name = @base_image_path.to_s
         | 
| 28 | 
            -
                      @annotated_base_image_path = @base_image_path.sub_ext(".diff.png")
         | 
| 29 | 
            -
             | 
| 30 23 | 
             
                      @driver_options = options.dup
         | 
| 31 24 |  | 
| 32 25 | 
             
                      @driver = Drivers.for(@driver_options)
         | 
| @@ -35,121 +28,116 @@ module Capybara | |
| 35 28 | 
             
                    # Compare the two image files and return `true` or `false` as quickly as possible.
         | 
| 36 29 | 
             
                    # Return falsely if the old file does not exist or the image dimensions do not match.
         | 
| 37 30 | 
             
                    def quick_equal?
         | 
| 38 | 
            -
                       | 
| 39 | 
            -
             | 
| 40 | 
            -
                      #  | 
| 31 | 
            +
                      require_images_exists!
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      # NOTE: This is very fuzzy logic, but so far it's helps to support current performance.
         | 
| 41 34 | 
             
                      return true if new_file_size == old_file_size
         | 
| 42 35 |  | 
| 43 36 | 
             
                      comparison = load_and_process_images
         | 
| 44 37 |  | 
| 45 38 | 
             
                      unless driver.same_dimension?(comparison)
         | 
| 46 | 
            -
                         | 
| 39 | 
            +
                        self.difference = build_failed_difference(comparison, {different_dimensions: true})
         | 
| 47 40 | 
             
                        return false
         | 
| 48 41 | 
             
                      end
         | 
| 49 42 |  | 
| 50 | 
            -
                       | 
| 43 | 
            +
                      if driver.same_pixels?(comparison)
         | 
| 44 | 
            +
                        self.difference = build_no_difference(comparison)
         | 
| 45 | 
            +
                        return true
         | 
| 46 | 
            +
                      end
         | 
| 51 47 |  | 
| 52 | 
            -
                      # Could not make any difference to be tolerable, so skip and return as not equal
         | 
| 48 | 
            +
                      # NOTE: Could not make any difference to be tolerable, so skip and return as not equal.
         | 
| 53 49 | 
             
                      return false if without_tolerable_options?
         | 
| 54 50 |  | 
| 55 | 
            -
                       | 
| 56 | 
            -
                      return true unless @difference.different?
         | 
| 51 | 
            +
                      self.difference = driver.find_difference_region(comparison)
         | 
| 57 52 |  | 
| 58 | 
            -
                       | 
| 59 | 
            -
                      false
         | 
| 53 | 
            +
                      !difference.different?
         | 
| 60 54 | 
             
                    end
         | 
| 61 55 |  | 
| 62 56 | 
             
                    # Compare the two image referenced by this object, and return `true` if they are different,
         | 
| 63 57 | 
             
                    # and `false` if they are the same.
         | 
| 64 58 | 
             
                    def different?
         | 
| 65 | 
            -
                       | 
| 66 | 
            -
             | 
| 67 | 
            -
                      @error_message = _different?
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                      clean_tmp_files unless @error_message
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                      !@error_message.nil?
         | 
| 59 | 
            +
                      processed.difference.different?
         | 
| 72 60 | 
             
                    end
         | 
| 73 61 |  | 
| 74 | 
            -
                    def  | 
| 75 | 
            -
                       | 
| 76 | 
            -
                        .map { |i| driver.dimension(i).join("x") }
         | 
| 77 | 
            -
                        .join(" => ")
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                      "Screenshot dimension has been changed for #{@new_file_name}: #{change_msg}"
         | 
| 62 | 
            +
                    def dimensions_changed?
         | 
| 63 | 
            +
                      difference.failed_by&.[](:different_dimensions)
         | 
| 80 64 | 
             
                    end
         | 
| 81 65 |  | 
| 82 | 
            -
                    def  | 
| 83 | 
            -
                      @ | 
| 84 | 
            -
             | 
| 66 | 
            +
                    def reporter
         | 
| 67 | 
            +
                      @reporter ||= begin
         | 
| 68 | 
            +
                        current_difference = difference || build_no_difference(nil)
         | 
| 69 | 
            +
                        Capybara::Screenshot::Diff::Reporters::Default.new(current_difference)
         | 
| 70 | 
            +
                      end
         | 
| 85 71 | 
             
                    end
         | 
| 86 72 |  | 
| 87 | 
            -
                    def  | 
| 88 | 
            -
                       | 
| 73 | 
            +
                    def processed?
         | 
| 74 | 
            +
                      !!difference
         | 
| 89 75 | 
             
                    end
         | 
| 90 76 |  | 
| 91 | 
            -
                    def  | 
| 92 | 
            -
                       | 
| 77 | 
            +
                    def processed
         | 
| 78 | 
            +
                      self.difference = find_difference unless processed?
         | 
| 79 | 
            +
                      @error_message ||= reporter.generate
         | 
| 80 | 
            +
                      self
         | 
| 93 81 | 
             
                    end
         | 
| 94 82 |  | 
| 95 | 
            -
                    NEW_LINE = "\n"
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                    attr_reader :error_message
         | 
| 98 | 
            -
             | 
| 99 83 | 
             
                    private
         | 
| 100 84 |  | 
| 101 | 
            -
                    def  | 
| 102 | 
            -
                       | 
| 103 | 
            -
                    end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                    def _different?
         | 
| 106 | 
            -
                      raise "There is no original (base) screenshot version to compare, located: #{@base_image_path}" unless @base_image_path.exist?
         | 
| 107 | 
            -
                      raise "There is no new screenshot version to compare, located: #{@image_path}" unless @image_path.exist?
         | 
| 85 | 
            +
                    def find_difference
         | 
| 86 | 
            +
                      require_images_exists!
         | 
| 108 87 |  | 
| 109 88 | 
             
                      comparison = load_and_process_images
         | 
| 110 89 |  | 
| 111 90 | 
             
                      unless driver.same_dimension?(comparison)
         | 
| 112 | 
            -
                        return  | 
| 91 | 
            +
                        return build_failed_difference(comparison, {different_dimensions: true})
         | 
| 113 92 | 
             
                      end
         | 
| 114 93 |  | 
| 115 | 
            -
                       | 
| 94 | 
            +
                      if driver.same_pixels?(comparison)
         | 
| 95 | 
            +
                        build_no_difference(comparison)
         | 
| 96 | 
            +
                      else
         | 
| 97 | 
            +
                        driver.find_difference_region(comparison)
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
                    end
         | 
| 116 100 |  | 
| 117 | 
            -
             | 
| 118 | 
            -
                       | 
| 101 | 
            +
                    def require_images_exists!
         | 
| 102 | 
            +
                      raise ArgumentError, "There is no original (base) screenshot version to compare, located: #{base_image_path}" unless base_image_path.exist?
         | 
| 103 | 
            +
                      raise ArgumentError, "There is no new screenshot version to compare, located: #{image_path}" unless image_path.exist?
         | 
| 104 | 
            +
                    end
         | 
| 119 105 |  | 
| 120 | 
            -
             | 
| 106 | 
            +
                    def difference=(new_difference)
         | 
| 107 | 
            +
                      @error_message = nil
         | 
| 108 | 
            +
                      @reporter = nil
         | 
| 109 | 
            +
                      @difference = new_difference
         | 
| 121 110 | 
             
                    end
         | 
| 122 111 |  | 
| 123 | 
            -
                    def  | 
| 124 | 
            -
                       | 
| 125 | 
            -
                      base_image, new_image = preprocess_images(images)
         | 
| 126 | 
            -
                      Comparison.new(new_image, base_image, @driver_options)
         | 
| 112 | 
            +
                    def image_files_exist?
         | 
| 113 | 
            +
                      @base_image_path.exist? && @image_path.exist?
         | 
| 127 114 | 
             
                    end
         | 
| 128 115 |  | 
| 129 | 
            -
                    def  | 
| 130 | 
            -
                       | 
| 131 | 
            -
                        "(#{difference.inspect})",
         | 
| 132 | 
            -
                        new_file_name,
         | 
| 133 | 
            -
                        annotated_base_image_path.to_path,
         | 
| 134 | 
            -
                        annotated_image_path.to_path
         | 
| 135 | 
            -
                      ].join(NEW_LINE)
         | 
| 116 | 
            +
                    def without_tolerable_options?
         | 
| 117 | 
            +
                      (@driver_options.keys & TOLERABLE_OPTIONS).empty?
         | 
| 136 118 | 
             
                    end
         | 
| 137 119 |  | 
| 138 | 
            -
                    def  | 
| 139 | 
            -
                       | 
| 120 | 
            +
                    def build_failed_difference(comparison, failed_by)
         | 
| 121 | 
            +
                      Difference.new(
         | 
| 122 | 
            +
                        nil,
         | 
| 123 | 
            +
                        {difference_level: nil, max_color_distance: 0},
         | 
| 124 | 
            +
                        comparison,
         | 
| 125 | 
            +
                        failed_by
         | 
| 126 | 
            +
                      )
         | 
| 140 127 | 
             
                    end
         | 
| 141 128 |  | 
| 142 | 
            -
                    def  | 
| 143 | 
            -
                       | 
| 129 | 
            +
                    def load_and_process_images
         | 
| 130 | 
            +
                      images = driver.load_images(base_image_path, image_path)
         | 
| 131 | 
            +
                      base_image, new_image = preprocess_images(images)
         | 
| 132 | 
            +
                      Comparison.new(new_image, base_image, @driver_options, driver, image_path, base_image_path)
         | 
| 144 133 | 
             
                    end
         | 
| 145 134 |  | 
| 146 | 
            -
                    def  | 
| 147 | 
            -
                      @driver_options[: | 
| 135 | 
            +
                    def skip_area
         | 
| 136 | 
            +
                      @driver_options[:skip_area]
         | 
| 148 137 | 
             
                    end
         | 
| 149 138 |  | 
| 150 | 
            -
                    def  | 
| 151 | 
            -
                       | 
| 152 | 
            -
                      build_error_message(difference)
         | 
| 139 | 
            +
                    def median_filter_window_size
         | 
| 140 | 
            +
                      @driver_options[:median_filter_window_size]
         | 
| 153 141 | 
             
                    end
         | 
| 154 142 |  | 
| 155 143 | 
             
                    def preprocess_images(images)
         | 
| @@ -159,17 +147,19 @@ module Capybara | |
| 159 147 | 
             
                    def preprocess_image(image)
         | 
| 160 148 | 
             
                      result = image
         | 
| 161 149 |  | 
| 162 | 
            -
                      # FIXME: How can we access to this method from public interface? Is this not documented feature?
         | 
| 163 | 
            -
                      if dimensions && driver.inscribed?(dimensions, result)
         | 
| 164 | 
            -
                        result = driver.crop(dimensions, result)
         | 
| 165 | 
            -
                      end
         | 
| 166 | 
            -
             | 
| 167 150 | 
             
                      if skip_area
         | 
| 168 151 | 
             
                        result = ignore_skipped_area(result)
         | 
| 169 152 | 
             
                      end
         | 
| 170 153 |  | 
| 171 154 | 
             
                      if median_filter_window_size
         | 
| 172 | 
            -
                         | 
| 155 | 
            +
                        if driver.is_a?(Drivers::VipsDriver)
         | 
| 156 | 
            +
                          result = blur_image_by(image, median_filter_window_size)
         | 
| 157 | 
            +
                        else
         | 
| 158 | 
            +
                          warn(
         | 
| 159 | 
            +
                            "[capybara-screenshot-diff] Median filter has been skipped for #{image_path} " \
         | 
| 160 | 
            +
                              "because it is not supported by #{driver.class.name}"
         | 
| 161 | 
            +
                          )
         | 
| 162 | 
            +
                        end
         | 
| 173 163 | 
             
                      end
         | 
| 174 164 |  | 
| 175 165 | 
             
                      result
         | 
| @@ -180,48 +170,28 @@ module Capybara | |
| 180 170 | 
             
                    end
         | 
| 181 171 |  | 
| 182 172 | 
             
                    def ignore_skipped_area(image)
         | 
| 183 | 
            -
                      skip_area | 
| 173 | 
            +
                      skip_area&.reduce(image) { |memo, region| driver.add_black_box(memo, region) }
         | 
| 184 174 | 
             
                    end
         | 
| 185 175 |  | 
| 186 176 | 
             
                    def old_file_size
         | 
| 187 | 
            -
                       | 
| 177 | 
            +
                      base_image_path.size
         | 
| 188 178 | 
             
                    end
         | 
| 189 179 |  | 
| 190 180 | 
             
                    def new_file_size
         | 
| 191 | 
            -
                       | 
| 192 | 
            -
                    end
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                    def not_different
         | 
| 195 | 
            -
                      nil
         | 
| 181 | 
            +
                      image_path.size
         | 
| 196 182 | 
             
                    end
         | 
| 197 183 |  | 
| 198 | 
            -
                    def  | 
| 199 | 
            -
                       | 
| 200 | 
            -
             | 
| 184 | 
            +
                    def build_no_difference(comparison = nil)
         | 
| 185 | 
            +
                      Difference.new(
         | 
| 186 | 
            +
                        nil,
         | 
| 187 | 
            +
                        {difference_level: nil, max_color_distance: 0},
         | 
| 188 | 
            +
                        comparison || build_comparison
         | 
| 189 | 
            +
                      ).freeze
         | 
| 201 190 | 
             
                    end
         | 
| 202 191 |  | 
| 203 | 
            -
                    def  | 
| 204 | 
            -
                       | 
| 205 | 
            -
                      image = annotate_skip_areas(image, difference.skip_area) if difference.skip_area
         | 
| 206 | 
            -
                      save(image, image_path.to_path)
         | 
| 207 | 
            -
                    end
         | 
| 208 | 
            -
             | 
| 209 | 
            -
                    DIFF_COLOR = [255, 0, 0, 255].freeze
         | 
| 210 | 
            -
             | 
| 211 | 
            -
                    def annotate_difference(image, region)
         | 
| 212 | 
            -
                      driver.draw_rectangles(Array[image], region, DIFF_COLOR, offset: 1).first
         | 
| 192 | 
            +
                    def build_comparison
         | 
| 193 | 
            +
                      Capybara::Screenshot::Diff::Comparison.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze
         | 
| 213 194 | 
             
                    end
         | 
| 214 | 
            -
             | 
| 215 | 
            -
                    SKIP_COLOR = [255, 192, 0, 255].freeze
         | 
| 216 | 
            -
             | 
| 217 | 
            -
                    def annotate_skip_areas(image, skip_areas)
         | 
| 218 | 
            -
                      skip_areas.reduce(image) do |memo, region|
         | 
| 219 | 
            -
                        driver.draw_rectangles(Array[memo], region, SKIP_COLOR).first
         | 
| 220 | 
            -
                      end
         | 
| 221 | 
            -
                    end
         | 
| 222 | 
            -
                  end
         | 
| 223 | 
            -
             | 
| 224 | 
            -
                  class Comparison < Struct.new(:new_image, :base_image, :options)
         | 
| 225 195 | 
             
                  end
         | 
| 226 196 | 
             
                end
         | 
| 227 197 | 
             
              end
         |