capybara-screenshot-diff 1.6.2 → 1.8.3
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 +29 -0
 - data/capybara-screenshot-diff.gemspec +6 -3
 - data/gems.rb +8 -2
 - data/lib/capybara/screenshot/diff/browser_helpers.rb +102 -0
 - data/lib/capybara/screenshot/diff/cucumber.rb +11 -0
 - data/lib/capybara/screenshot/diff/difference.rb +63 -0
 - data/lib/capybara/screenshot/diff/drivers/base_driver.rb +42 -0
 - data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +193 -252
 - data/lib/capybara/screenshot/diff/drivers/utils.rb +25 -0
 - data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +65 -100
 - data/lib/capybara/screenshot/diff/drivers.rb +16 -0
 - data/lib/capybara/screenshot/diff/image_compare.rb +138 -154
 - data/lib/capybara/screenshot/diff/os.rb +1 -1
 - data/lib/capybara/screenshot/diff/region.rb +86 -0
 - data/lib/capybara/screenshot/diff/screenshot_matcher.rb +128 -0
 - data/lib/capybara/screenshot/diff/screenshoter.rb +136 -0
 - data/lib/capybara/screenshot/diff/stabilization.rb +0 -210
 - data/lib/capybara/screenshot/diff/stable_screenshoter.rb +106 -0
 - data/lib/capybara/screenshot/diff/test_methods.rb +57 -63
 - data/lib/capybara/screenshot/diff/vcs.rb +48 -21
 - data/lib/capybara/screenshot/diff/version.rb +1 -1
 - data/lib/capybara/screenshot/diff.rb +38 -35
 - data/sig/capybara/screenshot/diff/diff.rbs +28 -0
 - data/sig/capybara/screenshot/diff/difference.rbs +33 -0
 - data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +63 -0
 - data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +36 -0
 - data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +89 -0
 - data/sig/capybara/screenshot/diff/drivers/utils.rbs +13 -0
 - data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +25 -0
 - data/sig/capybara/screenshot/diff/image_compare.rbs +93 -0
 - data/sig/capybara/screenshot/diff/os.rbs +11 -0
 - data/sig/capybara/screenshot/diff/region.rbs +43 -0
 - data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +60 -0
 - data/sig/capybara/screenshot/diff/screenshoter.rbs +48 -0
 - data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +29 -0
 - data/sig/capybara/screenshot/diff/test_methods.rbs +39 -0
 - data/sig/capybara/screenshot/diff/vcs.rbs +17 -0
 - metadata +30 -25
 - data/.gitattributes +0 -4
 - data/.github/workflows/lint.yml +0 -25
 - data/.github/workflows/test.yml +0 -120
 - data/.gitignore +0 -12
 - data/.standard.yml +0 -12
 - data/CONTRIBUTING.md +0 -22
 - data/Dockerfile +0 -60
 - data/README.md +0 -555
 - data/bin/bundle +0 -114
 - data/bin/console +0 -15
 - data/bin/install-vips +0 -11
 - data/bin/rake +0 -27
 - data/bin/setup +0 -8
 - data/bin/standardrb +0 -29
 - data/gemfiles/rails52.gemfile +0 -6
 - data/gemfiles/rails60_gems.rb +0 -8
 - data/gemfiles/rails61_gems.rb +0 -7
 - data/gemfiles/rails70_gems.rb +0 -7
 - data/tmp/.keep +0 -0
 
| 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class Region
         
     | 
| 
      
 4 
     | 
    
         
            +
              attr_accessor :x, :y, :width, :height
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def initialize(x, y, width, height)
         
     | 
| 
      
 7 
     | 
    
         
            +
                @x, @y, @width, @height = x, y, width, height
         
     | 
| 
      
 8 
     | 
    
         
            +
              end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              def self.from_top_left_corner_coordinates(x, y, width, height)
         
     | 
| 
      
 11 
     | 
    
         
            +
                return nil unless x && y && width && height
         
     | 
| 
      
 12 
     | 
    
         
            +
                return nil if width < 0 || height < 0
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                Region.new(x, y, width, height)
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              def self.from_edge_coordinates(left, top, right, bottom)
         
     | 
| 
      
 18 
     | 
    
         
            +
                return nil unless left && top && right && bottom
         
     | 
| 
      
 19 
     | 
    
         
            +
                return nil if right < left || bottom < top
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                Region.new(left, top, right - left, bottom - top)
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
              def to_edge_coordinates
         
     | 
| 
      
 25 
     | 
    
         
            +
                [left, top, right, bottom]
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              def to_top_left_corner_coordinates
         
     | 
| 
      
 29 
     | 
    
         
            +
                [x, y, width, height]
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
              def top
         
     | 
| 
      
 33 
     | 
    
         
            +
                y
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              def bottom
         
     | 
| 
      
 37 
     | 
    
         
            +
                y + height
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
              def left
         
     | 
| 
      
 41 
     | 
    
         
            +
                x
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              def right
         
     | 
| 
      
 45 
     | 
    
         
            +
                x + width
         
     | 
| 
      
 46 
     | 
    
         
            +
              end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
              def size
         
     | 
| 
      
 49 
     | 
    
         
            +
                return 0 if width < 0 || height < 0
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                result = width * height
         
     | 
| 
      
 52 
     | 
    
         
            +
                result.zero? ? 1 : result
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
              def to_a
         
     | 
| 
      
 56 
     | 
    
         
            +
                [@x, @y, @width, @height]
         
     | 
| 
      
 57 
     | 
    
         
            +
              end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
              def find_intersect_with(region)
         
     | 
| 
      
 60 
     | 
    
         
            +
                return nil unless intersect?(region)
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                new_left = [x, region.x].max
         
     | 
| 
      
 63 
     | 
    
         
            +
                new_top = [y, region.y].max
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                Region.new(new_left, new_top, [right, region.right].min - new_left, [bottom, region.bottom].min - new_top)
         
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
              def intersect?(region)
         
     | 
| 
      
 69 
     | 
    
         
            +
                left <= region.right && right >= region.left && top <= region.bottom && bottom >= region.top
         
     | 
| 
      
 70 
     | 
    
         
            +
              end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
              def move_by(right_by, down_by)
         
     | 
| 
      
 73 
     | 
    
         
            +
                Region.new(x + right_by, y + down_by, width, height)
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
              def find_relative_intersect(region)
         
     | 
| 
      
 77 
     | 
    
         
            +
                intersect = find_intersect_with(region)
         
     | 
| 
      
 78 
     | 
    
         
            +
                return nil unless intersect
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                intersect.move_by(-x, -y)
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
              def cover?(x, y)
         
     | 
| 
      
 84 
     | 
    
         
            +
                left <= x && x <= right && top <= y && y <= bottom
         
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,128 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "screenshoter"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "stable_screenshoter"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative "browser_helpers"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "vcs"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            module Capybara
         
     | 
| 
      
 9 
     | 
    
         
            +
              module Screenshot
         
     | 
| 
      
 10 
     | 
    
         
            +
                module Diff
         
     | 
| 
      
 11 
     | 
    
         
            +
                  class ScreenshotMatcher
         
     | 
| 
      
 12 
     | 
    
         
            +
                    attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    def initialize(screenshot_full_name, options = {})
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @screenshot_full_name = screenshot_full_name
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @driver_options = Diff.default_options.merge(options)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                      @screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".png")
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @base_screenshot_path = ScreenshotMatcher.base_image_path_from(@screenshot_path)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    def build_screenshot_matches_job
         
     | 
| 
      
 23 
     | 
    
         
            +
                      # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
         
     | 
| 
      
 24 
     | 
    
         
            +
                      return if BrowserHelpers.window_size_is_wrong?(Screenshot.window_size)
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                      # Stability Screenshoter Options
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                      # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
         
     | 
| 
      
 29 
     | 
    
         
            +
                      crop = calculate_crop_region(driver_options)
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                      # Allow nil or single or multiple areas
         
     | 
| 
      
 32 
     | 
    
         
            +
                      # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
         
     | 
| 
      
 33 
     | 
    
         
            +
                      if driver_options[:skip_area]
         
     | 
| 
      
 34 
     | 
    
         
            +
                        # Cast skip area args to Region and makes relative to crop
         
     | 
| 
      
 35 
     | 
    
         
            +
                        driver_options[:skip_area] = calculate_skip_area(driver_options[:skip_area], crop)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      end
         
     | 
| 
      
 37 
     | 
    
         
            +
                      driver_options[:driver] = Drivers.for(driver_options)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                      create_output_directory_for(screenshot_path) unless screenshot_path.exist?
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                      checkout_base_screenshot
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                      capture_options = {
         
     | 
| 
      
 44 
     | 
    
         
            +
                        crop: crop,
         
     | 
| 
      
 45 
     | 
    
         
            +
                        stability_time_limit: driver_options.delete(:stability_time_limit),
         
     | 
| 
      
 46 
     | 
    
         
            +
                        wait: driver_options.delete(:wait)
         
     | 
| 
      
 47 
     | 
    
         
            +
                      }
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                      take_comparison_screenshot(capture_options, driver_options, screenshot_path)
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                      return unless base_screenshot_path.exist?
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                      # Add comparison job in the queue
         
     | 
| 
      
 54 
     | 
    
         
            +
                      [
         
     | 
| 
      
 55 
     | 
    
         
            +
                        screenshot_full_name,
         
     | 
| 
      
 56 
     | 
    
         
            +
                        ImageCompare.new(screenshot_path.to_s, base_screenshot_path.to_s, driver_options)
         
     | 
| 
      
 57 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    def cleanup
         
     | 
| 
      
 61 
     | 
    
         
            +
                      FileUtils.rm_f(base_screenshot_path)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    def self.base_image_path_from(screenshot_path)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      screenshot_path.sub_ext(".base.png")
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    private
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                    def checkout_base_screenshot
         
     | 
| 
      
 71 
     | 
    
         
            +
                      Vcs.checkout_vcs(screenshot_path, base_screenshot_path)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    def calculate_crop_region(driver_options)
         
     | 
| 
      
 75 
     | 
    
         
            +
                      crop_coordinates = driver_options.delete(:crop)
         
     | 
| 
      
 76 
     | 
    
         
            +
                      return nil unless crop_coordinates
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                      crop_coordinates = BrowserHelpers.bounds_for_css(crop_coordinates).first if crop_coordinates.is_a?(String)
         
     | 
| 
      
 79 
     | 
    
         
            +
                      Region.from_edge_coordinates(*crop_coordinates)
         
     | 
| 
      
 80 
     | 
    
         
            +
                    end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                    def create_output_directory_for(screenshot_path)
         
     | 
| 
      
 83 
     | 
    
         
            +
                      screenshot_path.dirname.mkpath
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    # Try to get screenshot from browser.
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
         
     | 
| 
      
 88 
     | 
    
         
            +
                    # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
         
     | 
| 
      
 89 
     | 
    
         
            +
                    def take_comparison_screenshot(capture_options, driver_options, screenshot_path)
         
     | 
| 
      
 90 
     | 
    
         
            +
                      screenshoter = build_screenshoter_for(capture_options, driver_options)
         
     | 
| 
      
 91 
     | 
    
         
            +
                      screenshoter.take_comparison_screenshot(screenshot_path)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                    def build_screenshoter_for(capture_options, comparison_options = {})
         
     | 
| 
      
 95 
     | 
    
         
            +
                      if capture_options[:stability_time_limit]
         
     | 
| 
      
 96 
     | 
    
         
            +
                        StableScreenshoter.new(capture_options, comparison_options)
         
     | 
| 
      
 97 
     | 
    
         
            +
                      else
         
     | 
| 
      
 98 
     | 
    
         
            +
                        Diff.screenshoter.new(capture_options, comparison_options[:driver])
         
     | 
| 
      
 99 
     | 
    
         
            +
                      end
         
     | 
| 
      
 100 
     | 
    
         
            +
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    # Cast skip areas params into Region
         
     | 
| 
      
 103 
     | 
    
         
            +
                    # and if there is crop then makes absolute coordinates to eb relative to crop top left corner
         
     | 
| 
      
 104 
     | 
    
         
            +
                    def calculate_skip_area(skip_area, crop)
         
     | 
| 
      
 105 
     | 
    
         
            +
                      crop_region = crop && Region.new(*crop)
         
     | 
| 
      
 106 
     | 
    
         
            +
                      skip_area = Array(skip_area)
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                      css_selectors, regions = skip_area.compact.partition { |region| region.is_a? String }
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                      result = []
         
     | 
| 
      
 111 
     | 
    
         
            +
                      unless css_selectors.empty?
         
     | 
| 
      
 112 
     | 
    
         
            +
                        result.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors)))
         
     | 
| 
      
 113 
     | 
    
         
            +
                      end
         
     | 
| 
      
 114 
     | 
    
         
            +
                      result.concat(build_regions_for(regions.flatten.each_slice(4))) unless regions.empty?
         
     | 
| 
      
 115 
     | 
    
         
            +
                      result.compact!
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                      result.map! { |region| crop_region.find_relative_intersect(region) } if crop_region
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                      result
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                    def build_regions_for(coordinates)
         
     | 
| 
      
 123 
     | 
    
         
            +
                      coordinates.map { |coordinates_entity| Region.from_edge_coordinates(*coordinates_entity) }
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
                  end
         
     | 
| 
      
 126 
     | 
    
         
            +
                end
         
     | 
| 
      
 127 
     | 
    
         
            +
              end
         
     | 
| 
      
 128 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,136 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "os"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "browser_helpers"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Capybara
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Screenshot
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Screenshoter
         
     | 
| 
      
 9 
     | 
    
         
            +
                  attr_reader :capture_options, :comparison_options, :driver
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def initialize(capture_options, driver)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @capture_options = capture_options
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @comparison_options = comparison_options
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @driver = driver
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  def crop
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @capture_options[:crop]
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def wait
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @capture_options[:wait]
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def self.attempts_screenshot_paths(base_file)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    Dir["#{base_file.to_s.chomp(".png")}.attempt_*.png"].sort
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def self.cleanup_attempts_screenshots(base_file)
         
     | 
| 
      
 30 
     | 
    
         
            +
                    FileUtils.rm_rf attempts_screenshot_paths(base_file)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  # Try to get screenshot from browser.
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
         
     | 
| 
      
 36 
     | 
    
         
            +
                  def take_comparison_screenshot(screenshot_path)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    take_screenshot(new_screenshot_path)
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                    FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    Screenshoter.cleanup_attempts_screenshots(screenshot_path)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def self.gen_next_attempt_path(screenshot_path, iteration)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    Pathname.new(screenshot_path).sub_ext(format(".attempt_%02i.png", iteration))
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def take_screenshot(screenshot_path)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    blurred_input = prepare_page_for_screenshot(timeout: wait)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    # Take browser screenshot and save
         
     | 
| 
      
 53 
     | 
    
         
            +
                    browser_save_screenshot(screenshot_path)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    # Load saved screenshot and pre-process it
         
     | 
| 
      
 56 
     | 
    
         
            +
                    process_screenshot(screenshot_path)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 58 
     | 
    
         
            +
                    blurred_input&.click
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  def browser_save_screenshot(screenshot_path)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    BrowserHelpers.session.save_screenshot(screenshot_path)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  def process_screenshot(screenshot_path)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    # TODO(uwe): Remove when chromedriver takes right size screenshots
         
     | 
| 
      
 67 
     | 
    
         
            +
                    # TODO: Adds tests when this case is true
         
     | 
| 
      
 68 
     | 
    
         
            +
                    if selenium_with_retina_screen?
         
     | 
| 
      
 69 
     | 
    
         
            +
                      reduce_retina_image_size(screenshot_path)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
                    # ODOT
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                    if crop
         
     | 
| 
      
 74 
     | 
    
         
            +
                      image = driver.from_file(screenshot_path)
         
     | 
| 
      
 75 
     | 
    
         
            +
                      cropped_image = driver.crop(crop, image)
         
     | 
| 
      
 76 
     | 
    
         
            +
                      driver.save_image_to(cropped_image, screenshot_path)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    end
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  def reduce_retina_image_size(file_name)
         
     | 
| 
      
 81 
     | 
    
         
            +
                    expected_image_width = Screenshot.window_size[0]
         
     | 
| 
      
 82 
     | 
    
         
            +
                    saved_image = driver.from_file(file_name.to_s)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    return if driver.width_for(saved_image) < expected_image_width * 2
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                    notice_how_to_avoid_this
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    driver.save_image_to(resized_image, file_name)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  def notice_how_to_avoid_this
         
     | 
| 
      
 94 
     | 
    
         
            +
                    unless defined?(@_csd_retina_warned)
         
     | 
| 
      
 95 
     | 
    
         
            +
                      warn "Halving retina screenshot.  " \
         
     | 
| 
      
 96 
     | 
    
         
            +
                            'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
         
     | 
| 
      
 97 
     | 
    
         
            +
                      @_csd_retina_warned = true
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                  def prepare_page_for_screenshot(timeout:)
         
     | 
| 
      
 102 
     | 
    
         
            +
                    wait_images_loaded(timeout: timeout)
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    blurred_input = if Screenshot.blur_active_element
         
     | 
| 
      
 105 
     | 
    
         
            +
                      BrowserHelpers.blur_from_focused_element
         
     | 
| 
      
 106 
     | 
    
         
            +
                    end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                    if Screenshot.hide_caret
         
     | 
| 
      
 109 
     | 
    
         
            +
                      BrowserHelpers.hide_caret
         
     | 
| 
      
 110 
     | 
    
         
            +
                    end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                    blurred_input
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                  def wait_images_loaded(timeout:)
         
     | 
| 
      
 116 
     | 
    
         
            +
                    start = Time.now
         
     | 
| 
      
 117 
     | 
    
         
            +
                    loop do
         
     | 
| 
      
 118 
     | 
    
         
            +
                      pending_image = BrowserHelpers.pending_image_to_load
         
     | 
| 
      
 119 
     | 
    
         
            +
                      break unless pending_image
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                      if (Time.now - start) >= timeout
         
     | 
| 
      
 122 
     | 
    
         
            +
                        raise Capybara::Screenshot::Diff::ASSERTION, "Images not loaded after #{timeout}s: #{pending_image.inspect}"
         
     | 
| 
      
 123 
     | 
    
         
            +
                      end
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                      sleep 0.025
         
     | 
| 
      
 126 
     | 
    
         
            +
                    end
         
     | 
| 
      
 127 
     | 
    
         
            +
                  end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                  private
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                  def selenium_with_retina_screen?
         
     | 
| 
      
 132 
     | 
    
         
            +
                    Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
         
     | 
| 
      
 133 
     | 
    
         
            +
                  end
         
     | 
| 
      
 134 
     | 
    
         
            +
                end
         
     | 
| 
      
 135 
     | 
    
         
            +
              end
         
     | 
| 
      
 136 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,210 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # frozen_string_literal: true
         
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            require_relative "os"
         
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
            module Capybara
         
     | 
| 
       6 
     | 
    
         
            -
              module Screenshot
         
     | 
| 
       7 
     | 
    
         
            -
                module Diff
         
     | 
| 
       8 
     | 
    
         
            -
                  module Stabilization
         
     | 
| 
       9 
     | 
    
         
            -
                    include Os
         
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
                    IMAGE_WAIT_SCRIPT = <<-JS.strip_heredoc.freeze
         
     | 
| 
       12 
     | 
    
         
            -
                      function pending_image() {
         
     | 
| 
       13 
     | 
    
         
            -
                        var images = document.images;
         
     | 
| 
       14 
     | 
    
         
            -
                        for (var i = 0; i < images.length; i++) {
         
     | 
| 
       15 
     | 
    
         
            -
                          if (!images[i].complete) {
         
     | 
| 
       16 
     | 
    
         
            -
                              return images[i].src;
         
     | 
| 
       17 
     | 
    
         
            -
                          }
         
     | 
| 
       18 
     | 
    
         
            -
                        }
         
     | 
| 
       19 
     | 
    
         
            -
                        return false;
         
     | 
| 
       20 
     | 
    
         
            -
                      }()
         
     | 
| 
       21 
     | 
    
         
            -
                    JS
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
                    HIDE_CARET_SCRIPT = <<~JS
         
     | 
| 
       24 
     | 
    
         
            -
                      if (!document.getElementById('csdHideCaretStyle')) {
         
     | 
| 
       25 
     | 
    
         
            -
                        let style = document.createElement('style');
         
     | 
| 
       26 
     | 
    
         
            -
                        style.setAttribute('id', 'csdHideCaretStyle');
         
     | 
| 
       27 
     | 
    
         
            -
                        document.head.appendChild(style);
         
     | 
| 
       28 
     | 
    
         
            -
                        let styleSheet = style.sheet;
         
     | 
| 
       29 
     | 
    
         
            -
                        styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
         
     | 
| 
       30 
     | 
    
         
            -
                      }
         
     | 
| 
       31 
     | 
    
         
            -
                    JS
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
                    def take_stable_screenshot(comparison, stability_time_limit:, wait:, crop:)
         
     | 
| 
       34 
     | 
    
         
            -
                      previous_file_name = comparison.old_file_name
         
     | 
| 
       35 
     | 
    
         
            -
                      screenshot_started_at = last_image_change_at = Time.now
         
     | 
| 
       36 
     | 
    
         
            -
                      clean_stabilization_images(comparison.new_file_name)
         
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                      1.step do |i|
         
     | 
| 
       39 
     | 
    
         
            -
                        take_right_size_screenshot(comparison, crop: crop)
         
     | 
| 
       40 
     | 
    
         
            -
                        if comparison.quick_equal?
         
     | 
| 
       41 
     | 
    
         
            -
                          clean_stabilization_images(comparison.new_file_name)
         
     | 
| 
       42 
     | 
    
         
            -
                          break
         
     | 
| 
       43 
     | 
    
         
            -
                        end
         
     | 
| 
       44 
     | 
    
         
            -
                        comparison.reset
         
     | 
| 
       45 
     | 
    
         
            -
             
     | 
| 
       46 
     | 
    
         
            -
                        if previous_file_name
         
     | 
| 
       47 
     | 
    
         
            -
                          stabilization_comparison = make_stabilization_comparison_from(
         
     | 
| 
       48 
     | 
    
         
            -
                            comparison,
         
     | 
| 
       49 
     | 
    
         
            -
                            comparison.new_file_name,
         
     | 
| 
       50 
     | 
    
         
            -
                            previous_file_name
         
     | 
| 
       51 
     | 
    
         
            -
                          )
         
     | 
| 
       52 
     | 
    
         
            -
                          if stabilization_comparison.quick_equal?
         
     | 
| 
       53 
     | 
    
         
            -
                            if (Time.now - last_image_change_at) > stability_time_limit
         
     | 
| 
       54 
     | 
    
         
            -
                              clean_stabilization_images(comparison.new_file_name)
         
     | 
| 
       55 
     | 
    
         
            -
                              break
         
     | 
| 
       56 
     | 
    
         
            -
                            end
         
     | 
| 
       57 
     | 
    
         
            -
                            next
         
     | 
| 
       58 
     | 
    
         
            -
                          else
         
     | 
| 
       59 
     | 
    
         
            -
                            last_image_change_at = Time.now
         
     | 
| 
       60 
     | 
    
         
            -
                          end
         
     | 
| 
       61 
     | 
    
         
            -
                        end
         
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
                        previous_file_name = build_snapshot_version_file_name(
         
     | 
| 
       64 
     | 
    
         
            -
                          comparison,
         
     | 
| 
       65 
     | 
    
         
            -
                          i,
         
     | 
| 
       66 
     | 
    
         
            -
                          screenshot_started_at,
         
     | 
| 
       67 
     | 
    
         
            -
                          stabilization_comparison
         
     | 
| 
       68 
     | 
    
         
            -
                        )
         
     | 
| 
       69 
     | 
    
         
            -
             
     | 
| 
       70 
     | 
    
         
            -
                        FileUtils.mv(comparison.new_file_name, previous_file_name)
         
     | 
| 
       71 
     | 
    
         
            -
             
     | 
| 
       72 
     | 
    
         
            -
                        check_max_wait_time(
         
     | 
| 
       73 
     | 
    
         
            -
                          comparison,
         
     | 
| 
       74 
     | 
    
         
            -
                          screenshot_started_at,
         
     | 
| 
       75 
     | 
    
         
            -
                          max_wait_time: max_wait_time(comparison.shift_distance_limit, wait)
         
     | 
| 
       76 
     | 
    
         
            -
                        )
         
     | 
| 
       77 
     | 
    
         
            -
                      end
         
     | 
| 
       78 
     | 
    
         
            -
                    end
         
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
                    def notice_how_to_avoid_this
         
     | 
| 
       81 
     | 
    
         
            -
                      unless @_csd_retina_warned
         
     | 
| 
       82 
     | 
    
         
            -
                        warn "Halving retina screenshot.  " \
         
     | 
| 
       83 
     | 
    
         
            -
                            'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
         
     | 
| 
       84 
     | 
    
         
            -
                        @_csd_retina_warned = true
         
     | 
| 
       85 
     | 
    
         
            -
                      end
         
     | 
| 
       86 
     | 
    
         
            -
                    end
         
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
       88 
     | 
    
         
            -
                    private
         
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
                    def build_snapshot_version_file_name(comparison, iteration, screenshot_started_at, stabilization_comparison)
         
     | 
| 
       91 
     | 
    
         
            -
                      "#{comparison.new_file_name.chomp(".png")}" \
         
     | 
| 
       92 
     | 
    
         
            -
                            "_x#{format("%02i", iteration)}_#{(Time.now - screenshot_started_at).round(1)}s" \
         
     | 
| 
       93 
     | 
    
         
            -
                            "_#{stabilization_comparison.difference_region&.to_s&.gsub(", ", "_") || :initial}.png" \
         
     | 
| 
       94 
     | 
    
         
            -
                            "#{ImageCompare::TMP_FILE_SUFFIX}"
         
     | 
| 
       95 
     | 
    
         
            -
                    end
         
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
                    def make_stabilization_comparison_from(comparison, new_file_name, previous_file_name)
         
     | 
| 
       98 
     | 
    
         
            -
                      ImageCompare.new(new_file_name, previous_file_name, comparison.driver_options)
         
     | 
| 
       99 
     | 
    
         
            -
                    end
         
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
                    def reduce_retina_image_size(file_name, driver)
         
     | 
| 
       102 
     | 
    
         
            -
                      return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
         
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
                      expected_image_width = Capybara::Screenshot.window_size[0]
         
     | 
| 
       105 
     | 
    
         
            -
                      saved_image = driver.from_file(file_name)
         
     | 
| 
       106 
     | 
    
         
            -
                      return if driver.width_for(saved_image) < expected_image_width * 2
         
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
       108 
     | 
    
         
            -
                      notice_how_to_avoid_this
         
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
                      new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
         
     | 
| 
       111 
     | 
    
         
            -
                      resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
         
     | 
| 
       112 
     | 
    
         
            -
             
     | 
| 
       113 
     | 
    
         
            -
                      Dir.mktmpdir do |dir|
         
     | 
| 
       114 
     | 
    
         
            -
                        resized_image_file = "#{dir}/resized.png"
         
     | 
| 
       115 
     | 
    
         
            -
                        driver.save_image_to(resized_image, resized_image_file)
         
     | 
| 
       116 
     | 
    
         
            -
                        FileUtils.mv(resized_image_file, file_name)
         
     | 
| 
       117 
     | 
    
         
            -
                      end
         
     | 
| 
       118 
     | 
    
         
            -
                    end
         
     | 
| 
       119 
     | 
    
         
            -
             
     | 
| 
       120 
     | 
    
         
            -
                    def stabilization_images(base_file)
         
     | 
| 
       121 
     | 
    
         
            -
                      Dir["#{base_file.chomp(".png")}_x*.png#{ImageCompare::TMP_FILE_SUFFIX}"].sort
         
     | 
| 
       122 
     | 
    
         
            -
                    end
         
     | 
| 
       123 
     | 
    
         
            -
             
     | 
| 
       124 
     | 
    
         
            -
                    def clean_stabilization_images(base_file)
         
     | 
| 
       125 
     | 
    
         
            -
                      FileUtils.rm stabilization_images(base_file)
         
     | 
| 
       126 
     | 
    
         
            -
                    end
         
     | 
| 
       127 
     | 
    
         
            -
             
     | 
| 
       128 
     | 
    
         
            -
                    def prepare_page_for_screenshot(timeout:)
         
     | 
| 
       129 
     | 
    
         
            -
                      assert_images_loaded(timeout: timeout)
         
     | 
| 
       130 
     | 
    
         
            -
                      if Capybara::Screenshot.blur_active_element
         
     | 
| 
       131 
     | 
    
         
            -
                        active_element = execute_script(<<-JS)
         
     | 
| 
       132 
     | 
    
         
            -
                          ae = document.activeElement;
         
     | 
| 
       133 
     | 
    
         
            -
                          if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
         
     | 
| 
       134 
     | 
    
         
            -
                              ae.blur();
         
     | 
| 
       135 
     | 
    
         
            -
                              return ae;
         
     | 
| 
       136 
     | 
    
         
            -
                          }
         
     | 
| 
       137 
     | 
    
         
            -
                          return null;
         
     | 
| 
       138 
     | 
    
         
            -
                        JS
         
     | 
| 
       139 
     | 
    
         
            -
                        blurred_input = page.driver.send :unwrap_script_result, active_element
         
     | 
| 
       140 
     | 
    
         
            -
                      end
         
     | 
| 
       141 
     | 
    
         
            -
                      execute_script(HIDE_CARET_SCRIPT) if Capybara::Screenshot.hide_caret
         
     | 
| 
       142 
     | 
    
         
            -
                      blurred_input
         
     | 
| 
       143 
     | 
    
         
            -
                    end
         
     | 
| 
       144 
     | 
    
         
            -
             
     | 
| 
       145 
     | 
    
         
            -
                    def take_right_size_screenshot(comparison, crop:)
         
     | 
| 
       146 
     | 
    
         
            -
                      save_screenshot(comparison.new_file_name)
         
     | 
| 
       147 
     | 
    
         
            -
             
     | 
| 
       148 
     | 
    
         
            -
                      # TODO(uwe): Remove when chromedriver takes right size screenshots
         
     | 
| 
       149 
     | 
    
         
            -
                      reduce_retina_image_size(comparison.new_file_name, comparison.driver)
         
     | 
| 
       150 
     | 
    
         
            -
                      # ODOT
         
     | 
| 
       151 
     | 
    
         
            -
             
     | 
| 
       152 
     | 
    
         
            -
                      if crop
         
     | 
| 
       153 
     | 
    
         
            -
                        full_img = comparison.driver.from_file(comparison.new_file_name)
         
     | 
| 
       154 
     | 
    
         
            -
                        area_img = full_img.crop(crop[0], crop[1], crop[2] - crop[0], crop[3] - crop[1])
         
     | 
| 
       155 
     | 
    
         
            -
                        comparison.driver.save_image_to(area_img, comparison.new_file_name)
         
     | 
| 
       156 
     | 
    
         
            -
                      end
         
     | 
| 
       157 
     | 
    
         
            -
                    end
         
     | 
| 
       158 
     | 
    
         
            -
             
     | 
| 
       159 
     | 
    
         
            -
                    def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
         
     | 
| 
       160 
     | 
    
         
            -
                      return if (Time.now - screenshot_started_at) < max_wait_time
         
     | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
       162 
     | 
    
         
            -
                      annotate_stabilization_images(comparison)
         
     | 
| 
       163 
     | 
    
         
            -
                      # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
         
     | 
| 
       164 
     | 
    
         
            -
                      fail("Could not get stable screenshot within #{max_wait_time}s\n" \
         
     | 
| 
       165 
     | 
    
         
            -
                                "#{stabilization_images(comparison.new_file_name).join("\n")}")
         
     | 
| 
       166 
     | 
    
         
            -
                    end
         
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
     | 
    
         
            -
                    def annotate_stabilization_images(comparison)
         
     | 
| 
       169 
     | 
    
         
            -
                      previous_file = comparison.old_file_name
         
     | 
| 
       170 
     | 
    
         
            -
                      stabilization_images(comparison.new_file_name).each do |file_name|
         
     | 
| 
       171 
     | 
    
         
            -
                        if File.exist? previous_file
         
     | 
| 
       172 
     | 
    
         
            -
                          stabilization_comparison = make_stabilization_comparison_from(
         
     | 
| 
       173 
     | 
    
         
            -
                            comparison,
         
     | 
| 
       174 
     | 
    
         
            -
                            file_name,
         
     | 
| 
       175 
     | 
    
         
            -
                            previous_file
         
     | 
| 
       176 
     | 
    
         
            -
                          )
         
     | 
| 
       177 
     | 
    
         
            -
                          if stabilization_comparison.different?
         
     | 
| 
       178 
     | 
    
         
            -
                            FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
         
     | 
| 
       179 
     | 
    
         
            -
                          end
         
     | 
| 
       180 
     | 
    
         
            -
                          FileUtils.rm stabilization_comparison.annotated_old_file_name
         
     | 
| 
       181 
     | 
    
         
            -
                        end
         
     | 
| 
       182 
     | 
    
         
            -
                        previous_file = file_name
         
     | 
| 
       183 
     | 
    
         
            -
                      end
         
     | 
| 
       184 
     | 
    
         
            -
                    end
         
     | 
| 
       185 
     | 
    
         
            -
             
     | 
| 
       186 
     | 
    
         
            -
                    def max_wait_time(shift_distance_limit, wait)
         
     | 
| 
       187 
     | 
    
         
            -
                      shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
         
     | 
| 
       188 
     | 
    
         
            -
                      wait * shift_factor
         
     | 
| 
       189 
     | 
    
         
            -
                    end
         
     | 
| 
       190 
     | 
    
         
            -
             
     | 
| 
       191 
     | 
    
         
            -
                    def assert_images_loaded(timeout:)
         
     | 
| 
       192 
     | 
    
         
            -
                      return unless respond_to? :evaluate_script
         
     | 
| 
       193 
     | 
    
         
            -
             
     | 
| 
       194 
     | 
    
         
            -
                      start = Time.now
         
     | 
| 
       195 
     | 
    
         
            -
                      loop do
         
     | 
| 
       196 
     | 
    
         
            -
                        pending_image = evaluate_script IMAGE_WAIT_SCRIPT
         
     | 
| 
       197 
     | 
    
         
            -
                        break unless pending_image
         
     | 
| 
       198 
     | 
    
         
            -
             
     | 
| 
       199 
     | 
    
         
            -
                        assert(
         
     | 
| 
       200 
     | 
    
         
            -
                          (Time.now - start) < timeout,
         
     | 
| 
       201 
     | 
    
         
            -
                          "Images not loaded after #{timeout}s: #{pending_image.inspect}"
         
     | 
| 
       202 
     | 
    
         
            -
                        )
         
     | 
| 
       203 
     | 
    
         
            -
             
     | 
| 
       204 
     | 
    
         
            -
                        sleep 0.1
         
     | 
| 
       205 
     | 
    
         
            -
                      end
         
     | 
| 
       206 
     | 
    
         
            -
                    end
         
     | 
| 
       207 
     | 
    
         
            -
                  end
         
     | 
| 
       208 
     | 
    
         
            -
                end
         
     | 
| 
       209 
     | 
    
         
            -
              end
         
     | 
| 
       210 
     | 
    
         
            -
            end
         
     | 
| 
         @@ -0,0 +1,106 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Capybara
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Screenshot
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Diff
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class StableScreenshoter
         
     | 
| 
      
 7 
     | 
    
         
            +
                    STABILITY_OPTIONS = [:stability_time_limit, :wait]
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    attr_reader :stability_time_limit, :wait
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(capture_options, comparison_options = nil)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @stability_time_limit, @wait = capture_options.fetch_values(:stability_time_limit, :wait)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @comparison_options = comparison_options || Diff.default_options
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), @comparison_options[:driver])
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    # Try to get screenshot from browser.
         
     | 
| 
      
 18 
     | 
    
         
            +
                    # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
         
     | 
| 
      
 19 
     | 
    
         
            +
                    # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
         
     | 
| 
      
 20 
     | 
    
         
            +
                    def take_comparison_screenshot(screenshot_path)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      new_screenshot_path = take_stable_screenshot(screenshot_path)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      # We failed to get stable browser state! Generate difference between attempts to overview moving parts!
         
     | 
| 
      
 24 
     | 
    
         
            +
                      unless new_screenshot_path
         
     | 
| 
      
 25 
     | 
    
         
            +
                        # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
         
     | 
| 
      
 26 
     | 
    
         
            +
                        annotate_attempts_and_fail!(screenshot_path)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                      FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      Screenshoter.cleanup_attempts_screenshots(screenshot_path)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    def take_stable_screenshot(screenshot_path)
         
     | 
| 
      
 34 
     | 
    
         
            +
                      # We try to compare first attempt with checkout version, in order to not run next screenshots
         
     | 
| 
      
 35 
     | 
    
         
            +
                      attempt_path = nil
         
     | 
| 
      
 36 
     | 
    
         
            +
                      screenshot_started_at = last_attempt_at = Time.now
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                      # Cleanup all previous attempts for sure
         
     | 
| 
      
 39 
     | 
    
         
            +
                      Screenshoter.cleanup_attempts_screenshots(screenshot_path)
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                      0.step do |i|
         
     | 
| 
      
 42 
     | 
    
         
            +
                        # Prevents redundant screenshots generations
         
     | 
| 
      
 43 
     | 
    
         
            +
                        sleep(stability_time_limit) unless i == 0
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                        elapsed_time = last_attempt_at - screenshot_started_at
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                        prev_attempt_path = attempt_path
         
     | 
| 
      
 48 
     | 
    
         
            +
                        attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i)
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                        @screenshoter.take_screenshot(attempt_path)
         
     | 
| 
      
 51 
     | 
    
         
            +
                        last_attempt_at = Time.now
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                        next unless prev_attempt_path
         
     | 
| 
      
 54 
     | 
    
         
            +
                        stabilization_comparator = build_comparison_for(attempt_path, prev_attempt_path)
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                        # If previous screenshot is equal to the current, then we are good
         
     | 
| 
      
 57 
     | 
    
         
            +
                        return attempt_path if prev_attempt_path && stabilization_comparator.quick_equal?
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                        # If timeout then we failed to generate valid screenshot
         
     | 
| 
      
 60 
     | 
    
         
            +
                        return nil if timeout?(elapsed_time)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      end
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    private
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    def build_comparison_for(attempt_path, previous_attempt_path)
         
     | 
| 
      
 67 
     | 
    
         
            +
                      ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                    def annotate_attempts_and_fail!(screenshot_path)
         
     | 
| 
      
 71 
     | 
    
         
            +
                      screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path)
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                      annotate_stabilization_images(screenshot_attempts)
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                      # TODO: Move fail to the queue after tests passed
         
     | 
| 
      
 76 
     | 
    
         
            +
                      fail("Could not get stable screenshot within #{wait}s:\n#{screenshot_attempts.join("\n")}")
         
     | 
| 
      
 77 
     | 
    
         
            +
                    end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    # TODO: Add tests that we annotate all files except first one
         
     | 
| 
      
 80 
     | 
    
         
            +
                    def annotate_stabilization_images(attempts_screenshot_paths)
         
     | 
| 
      
 81 
     | 
    
         
            +
                      previous_file = nil
         
     | 
| 
      
 82 
     | 
    
         
            +
                      attempts_screenshot_paths.reverse_each do |file_name|
         
     | 
| 
      
 83 
     | 
    
         
            +
                        if previous_file && File.exist?(previous_file)
         
     | 
| 
      
 84 
     | 
    
         
            +
                          attempts_comparison = build_comparison_for(file_name, previous_file)
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                          if attempts_comparison.different?
         
     | 
| 
      
 87 
     | 
    
         
            +
                            FileUtils.mv(attempts_comparison.annotated_base_image_path, previous_file, force: true)
         
     | 
| 
      
 88 
     | 
    
         
            +
                          else
         
     | 
| 
      
 89 
     | 
    
         
            +
                            warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
         
     | 
| 
      
 90 
     | 
    
         
            +
                              "#{previous_file} and #{file_name} are equal"
         
     | 
| 
      
 91 
     | 
    
         
            +
                          end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                          FileUtils.rm(attempts_comparison.annotated_image_path, force: true)
         
     | 
| 
      
 94 
     | 
    
         
            +
                        end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                        previous_file = file_name
         
     | 
| 
      
 97 
     | 
    
         
            +
                      end
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                    def timeout?(elapsed_time)
         
     | 
| 
      
 101 
     | 
    
         
            +
                      elapsed_time > wait
         
     | 
| 
      
 102 
     | 
    
         
            +
                    end
         
     | 
| 
      
 103 
     | 
    
         
            +
                  end
         
     | 
| 
      
 104 
     | 
    
         
            +
                end
         
     | 
| 
      
 105 
     | 
    
         
            +
              end
         
     | 
| 
      
 106 
     | 
    
         
            +
            end
         
     |