capybara-screenshot-diff 1.8.3 → 1.9.0

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -11
  3. data/capybara-screenshot-diff.gemspec +1 -2
  4. data/gems.rb +4 -4
  5. data/lib/capybara/screenshot/diff/area_calculator.rb +56 -0
  6. data/lib/capybara/screenshot/diff/browser_helpers.rb +1 -1
  7. data/lib/capybara/screenshot/diff/comparison.rb +6 -0
  8. data/lib/capybara/screenshot/diff/cucumber.rb +1 -9
  9. data/lib/capybara/screenshot/diff/difference.rb +8 -4
  10. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +0 -4
  11. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +10 -5
  12. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +14 -3
  13. data/lib/capybara/screenshot/diff/drivers.rb +1 -1
  14. data/lib/capybara/screenshot/diff/image_compare.rb +80 -114
  15. data/lib/capybara/screenshot/diff/region.rb +28 -7
  16. data/lib/capybara/screenshot/diff/reporters/default.rb +117 -0
  17. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +24 -53
  18. data/lib/capybara/screenshot/diff/screenshoter.rb +61 -42
  19. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +51 -29
  20. data/lib/capybara/screenshot/diff/test_methods.rb +75 -8
  21. data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +0 -7
  22. data/lib/capybara/screenshot/diff/vcs.rb +1 -1
  23. data/lib/capybara/screenshot/diff/version.rb +1 -1
  24. data/lib/capybara/screenshot/diff.rb +1 -111
  25. data/lib/capybara-screenshot-diff.rb +1 -1
  26. data/lib/capybara_screenshot_diff/cucumber.rb +12 -0
  27. data/lib/capybara_screenshot_diff/dsl.rb +10 -0
  28. data/lib/capybara_screenshot_diff/minitest.rb +45 -0
  29. data/lib/capybara_screenshot_diff/rspec.rb +31 -0
  30. data/lib/capybara_screenshot_diff.rb +85 -0
  31. metadata +15 -37
  32. data/lib/capybara/screenshot/diff/stabilization.rb +0 -0
  33. data/sig/capybara/screenshot/diff/diff.rbs +0 -28
  34. data/sig/capybara/screenshot/diff/difference.rbs +0 -33
  35. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +0 -63
  36. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +0 -36
  37. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +0 -89
  38. data/sig/capybara/screenshot/diff/drivers/utils.rbs +0 -13
  39. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +0 -25
  40. data/sig/capybara/screenshot/diff/image_compare.rbs +0 -93
  41. data/sig/capybara/screenshot/diff/os.rbs +0 -11
  42. data/sig/capybara/screenshot/diff/region.rbs +0 -43
  43. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +0 -60
  44. data/sig/capybara/screenshot/diff/screenshoter.rbs +0 -48
  45. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +0 -29
  46. data/sig/capybara/screenshot/diff/test_methods.rbs +0 -39
  47. data/sig/capybara/screenshot/diff/vcs.rbs +0 -17
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Screenshot::Diff
4
+ module Reporters
5
+ class Default
6
+ attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path, :difference
7
+
8
+ def initialize(difference)
9
+ @difference = difference
10
+
11
+ screenshot_format = difference.comparison.options[:screenshot_format] || comparison.new_image_path.extname.slice(1..-1)
12
+ @annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
13
+ @annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
14
+ @heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
15
+ end
16
+
17
+ def generate
18
+ if difference.equal?
19
+ # NOTE: Delete previous run runtime files
20
+ clean_tmp_files
21
+ return nil
22
+ end
23
+
24
+ if difference.failed? && difference.failed_by[:different_dimensions]
25
+ return build_error_for_different_dimensions
26
+ end
27
+
28
+ annotate_and_save_images
29
+ build_error_message
30
+ end
31
+
32
+ def clean_tmp_files
33
+ annotated_base_image_path.unlink if annotated_base_image_path.exist?
34
+ annotated_image_path.unlink if annotated_image_path.exist?
35
+ end
36
+
37
+ def build_error_for_different_dimensions
38
+ change_msg = [comparison.base_image, comparison.new_image]
39
+ .map { |image| driver.dimension(image).join("x") }
40
+ .join(" => ")
41
+
42
+ "Screenshot dimension has been changed for #{image_path.to_path}: #{change_msg}"
43
+ end
44
+
45
+ def annotate_and_save_images
46
+ save_annotation_for(new_image, annotated_image_path)
47
+ save_annotation_for(base_image, annotated_base_image_path)
48
+ save_heatmap_diff if difference.meta[:diff_mask]
49
+ end
50
+
51
+ def save_annotation_for(image, image_path)
52
+ image = annotate_difference(image, difference.region)
53
+ image = annotate_skip_areas(image, difference.skip_area) if difference.skip_area
54
+
55
+ save(image, image_path.to_path)
56
+ end
57
+
58
+ DIFF_COLOR = [255, 0, 0, 255].freeze
59
+
60
+ def annotate_difference(image, region)
61
+ driver.draw_rectangles([image], region, DIFF_COLOR, offset: 1).first
62
+ end
63
+
64
+ SKIP_COLOR = [255, 192, 0, 255].freeze
65
+
66
+ def annotate_skip_areas(image, skip_areas)
67
+ skip_areas.reduce(image) do |memo, region|
68
+ driver.draw_rectangles([memo], region, SKIP_COLOR).first
69
+ end
70
+ end
71
+
72
+ def save(image, image_path)
73
+ driver.save_image_to(image, image_path.to_s)
74
+ end
75
+
76
+ NEW_LINE = "\n"
77
+
78
+ def build_error_message
79
+ [
80
+ "(#{difference.inspect})",
81
+ image_path.to_path,
82
+ annotated_base_image_path.to_path,
83
+ annotated_image_path.to_path
84
+ ].join(NEW_LINE)
85
+ end
86
+
87
+ private
88
+
89
+ def save_heatmap_diff
90
+ merged_image = driver.merge(new_image, base_image)
91
+ highlighted_mask = driver.highlight_mask(difference.meta[:diff_mask], merged_image, color: DIFF_COLOR)
92
+
93
+ save(highlighted_mask, heatmap_diff_path.to_path)
94
+ end
95
+
96
+ def base_image
97
+ difference.comparison.base_image
98
+ end
99
+
100
+ def new_image
101
+ difference.comparison.new_image
102
+ end
103
+
104
+ def image_path
105
+ comparison.new_image_path
106
+ end
107
+
108
+ def driver
109
+ @_driver ||= comparison.driver
110
+ end
111
+
112
+ def comparison
113
+ @_comparison ||= difference.comparison
114
+ end
115
+ end
116
+ end
117
+ end
@@ -4,18 +4,20 @@ require_relative "screenshoter"
4
4
  require_relative "stable_screenshoter"
5
5
  require_relative "browser_helpers"
6
6
  require_relative "vcs"
7
+ require_relative "area_calculator"
7
8
 
8
9
  module Capybara
9
10
  module Screenshot
10
11
  module Diff
11
12
  class ScreenshotMatcher
12
- attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path
13
+ attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path, :screenshot_format
13
14
 
14
15
  def initialize(screenshot_full_name, options = {})
15
16
  @screenshot_full_name = screenshot_full_name
16
17
  @driver_options = Diff.default_options.merge(options)
17
18
 
18
- @screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".png")
19
+ @screenshot_format = @driver_options[:screenshot_format]
20
+ @screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".#{screenshot_format}")
19
21
  @base_screenshot_path = ScreenshotMatcher.base_image_path_from(@screenshot_path)
20
22
  end
21
23
 
@@ -23,46 +25,47 @@ module Capybara
23
25
  # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
24
26
  return if BrowserHelpers.window_size_is_wrong?(Screenshot.window_size)
25
27
 
26
- # Stability Screenshoter Options
27
-
28
28
  # TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
29
- crop = calculate_crop_region(driver_options)
29
+ area_calculator = AreaCalculator.new(driver_options.delete(:crop), driver_options[:skip_area])
30
+ driver_options[:crop] = area_calculator.calculate_crop
30
31
 
31
- # Allow nil or single or multiple areas
32
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)
33
+ # Allow nil or single or multiple areas
34
+ driver_options[:skip_area] = area_calculator.calculate_skip_area
35
+
36
+ driver_options[:driver] = Drivers.for(driver_options[:driver])
38
37
 
38
+ # Load base screenshot from VCS
39
39
  create_output_directory_for(screenshot_path) unless screenshot_path.exist?
40
40
 
41
41
  checkout_base_screenshot
42
42
 
43
+ # When fail_if_new is true no need to create screenshot if base screenshot is missing
44
+ return if Capybara::Screenshot::Diff.fail_if_new && !base_screenshot_path.exist?
45
+
43
46
  capture_options = {
44
- crop: crop,
47
+ # screenshot options
48
+ capybara_screenshot_options: driver_options[:capybara_screenshot_options],
49
+ crop: driver_options.delete(:crop),
50
+ # delivery options
51
+ screenshot_format: driver_options[:screenshot_format],
52
+ # stability options
45
53
  stability_time_limit: driver_options.delete(:stability_time_limit),
46
54
  wait: driver_options.delete(:wait)
47
55
  }
48
56
 
57
+ # Load new screenshot from Browser
49
58
  take_comparison_screenshot(capture_options, driver_options, screenshot_path)
50
59
 
60
+ # Pre-computation: No need to compare without base screenshot
51
61
  return unless base_screenshot_path.exist?
52
62
 
53
63
  # 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)
64
+ [screenshot_full_name, ImageCompare.new(screenshot_path, base_screenshot_path, driver_options)]
62
65
  end
63
66
 
64
67
  def self.base_image_path_from(screenshot_path)
65
- screenshot_path.sub_ext(".base.png")
68
+ screenshot_path.sub_ext(".base#{screenshot_path.extname}")
66
69
  end
67
70
 
68
71
  private
@@ -71,14 +74,6 @@ module Capybara
71
74
  Vcs.checkout_vcs(screenshot_path, base_screenshot_path)
72
75
  end
73
76
 
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
77
  def create_output_directory_for(screenshot_path)
83
78
  screenshot_path.dirname.mkpath
84
79
  end
@@ -98,30 +93,6 @@ module Capybara
98
93
  Diff.screenshoter.new(capture_options, comparison_options[:driver])
99
94
  end
100
95
  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
96
  end
126
97
  end
127
98
  end
@@ -22,8 +22,17 @@ module Capybara
22
22
  @capture_options[:wait]
23
23
  end
24
24
 
25
+ def screenshot_format
26
+ @capture_options[:screenshot_format] || "png"
27
+ end
28
+
29
+ def capybara_screenshot_options
30
+ @capture_options[:capybara_screenshot_options] || {}
31
+ end
32
+
25
33
  def self.attempts_screenshot_paths(base_file)
26
- Dir["#{base_file.to_s.chomp(".png")}.attempt_*.png"].sort
34
+ extname = Pathname.new(base_file).extname
35
+ Dir["#{base_file.to_s.chomp(extname)}.attempt_*#{extname}"].sort
27
36
  end
28
37
 
29
38
  def self.cleanup_attempts_screenshots(base_file)
@@ -34,76 +43,51 @@ module Capybara
34
43
  # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
35
44
  # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
36
45
  def take_comparison_screenshot(screenshot_path)
37
- new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0)
46
+ capture_screenshot_at(screenshot_path)
38
47
 
39
- take_screenshot(new_screenshot_path)
40
-
41
- FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
42
48
  Screenshoter.cleanup_attempts_screenshots(screenshot_path)
43
49
  end
44
50
 
45
51
  def self.gen_next_attempt_path(screenshot_path, iteration)
46
- Pathname.new(screenshot_path).sub_ext(format(".attempt_%02i.png", iteration))
52
+ screenshot_path.sub_ext(format(".attempt_%02i#{screenshot_path.extname}", iteration))
47
53
  end
48
54
 
55
+ PNG_EXTENSION = ".png"
56
+
49
57
  def take_screenshot(screenshot_path)
50
58
  blurred_input = prepare_page_for_screenshot(timeout: wait)
51
59
 
52
60
  # Take browser screenshot and save
53
- browser_save_screenshot(screenshot_path)
61
+ save_and_process_screenshot(screenshot_path)
54
62
 
55
- # Load saved screenshot and pre-process it
56
- process_screenshot(screenshot_path)
57
- ensure
58
63
  blurred_input&.click
59
64
  end
60
65
 
61
- def browser_save_screenshot(screenshot_path)
62
- BrowserHelpers.session.save_screenshot(screenshot_path)
63
- end
66
+ def process_screenshot(stored_path, screenshot_path)
67
+ screenshot_image = driver.from_file(stored_path)
64
68
 
65
- def process_screenshot(screenshot_path)
66
69
  # TODO(uwe): Remove when chromedriver takes right size screenshots
67
70
  # TODO: Adds tests when this case is true
68
- if selenium_with_retina_screen?
69
- reduce_retina_image_size(screenshot_path)
70
- end
71
+ screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
71
72
  # ODOT
72
73
 
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
74
+ screenshot_image = driver.crop(crop, screenshot_image) if crop
84
75
 
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)
76
+ driver.save_image_to(screenshot_image, screenshot_path)
91
77
  end
92
78
 
93
79
  def notice_how_to_avoid_this
94
80
  unless defined?(@_csd_retina_warned)
95
81
  warn "Halving retina screenshot. " \
96
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
82
+ 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
97
83
  @_csd_retina_warned = true
98
84
  end
99
85
  end
100
86
 
101
87
  def prepare_page_for_screenshot(timeout:)
102
- wait_images_loaded(timeout: timeout)
88
+ wait_images_loaded(timeout: timeout) if timeout
103
89
 
104
- blurred_input = if Screenshot.blur_active_element
105
- BrowserHelpers.blur_from_focused_element
106
- end
90
+ blurred_input = BrowserHelpers.blur_from_focused_element if Screenshot.blur_active_element
107
91
 
108
92
  if Screenshot.hide_caret
109
93
  BrowserHelpers.hide_caret
@@ -113,13 +97,15 @@ module Capybara
113
97
  end
114
98
 
115
99
  def wait_images_loaded(timeout:)
116
- start = Time.now
100
+ return unless timeout
101
+
102
+ deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
117
103
  loop do
118
104
  pending_image = BrowserHelpers.pending_image_to_load
119
105
  break unless pending_image
120
106
 
121
- if (Time.now - start) >= timeout
122
- raise Capybara::Screenshot::Diff::ASSERTION, "Images not loaded after #{timeout}s: #{pending_image.inspect}"
107
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
108
+ raise CapybaraScreenshotDiff::ExpectationNotMet, "Images have not been loaded after #{timeout}s: #{pending_image.inspect}"
123
109
  end
124
110
 
125
111
  sleep 0.025
@@ -128,6 +114,39 @@ module Capybara
128
114
 
129
115
  private
130
116
 
117
+ def save_and_process_screenshot(screenshot_path)
118
+ tmpfile = Tempfile.new([screenshot_path.basename.to_s, PNG_EXTENSION])
119
+ BrowserHelpers.session.save_screenshot(tmpfile.path, **capybara_screenshot_options)
120
+ # Load saved screenshot and pre-process it
121
+ process_screenshot(tmpfile.path, screenshot_path)
122
+ ensure
123
+ File.unlink(tmpfile) if tmpfile
124
+ end
125
+
126
+ def capture_screenshot_at(screenshot_path)
127
+ new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0)
128
+ take_and_process_screenshot(new_screenshot_path, screenshot_path)
129
+ end
130
+
131
+ def take_and_process_screenshot(new_screenshot_path, screenshot_path)
132
+ take_screenshot(new_screenshot_path)
133
+ move_screenshot_to(new_screenshot_path, screenshot_path)
134
+ end
135
+
136
+ def move_screenshot_to(new_screenshot_path, screenshot_path)
137
+ FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
138
+ end
139
+
140
+ def resize_if_needed(saved_image)
141
+ expected_image_width = Screenshot.window_size[0]
142
+ return saved_image if driver.width_for(saved_image) < expected_image_width * 2
143
+
144
+ notice_how_to_avoid_this
145
+
146
+ new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
147
+ driver.resize_image_to(saved_image, expected_image_width, new_height)
148
+ end
149
+
131
150
  def selenium_with_retina_screen?
132
151
  Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
133
152
  end
@@ -8,15 +8,36 @@ module Capybara
8
8
 
9
9
  attr_reader :stability_time_limit, :wait
10
10
 
11
+ # Initializes a new instance of StableScreenshoter
12
+ #
13
+ # This method sets up a new screenshoter with specific capture and comparison options. It validates the presence of
14
+ # `:stability_time_limit` and `:wait` in capture options and ensures that `:stability_time_limit` is less than or equal to `:wait`.
15
+ #
16
+ # @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`.
17
+ # @param comparison_options [Hash, nil] The options for comparing screenshots, defaults to `nil` which uses `Diff.default_options`.
18
+ # @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`.
11
19
  def initialize(capture_options, comparison_options = nil)
12
20
  @stability_time_limit, @wait = capture_options.fetch_values(:stability_time_limit, :wait)
21
+
22
+ raise ArgumentError, "wait should be provided for stable screenshots" unless wait
23
+ raise ArgumentError, "stability_time_limit should be provided for stable screenshots" unless stability_time_limit
24
+ raise ArgumentError, "stability_time_limit (#{stability_time_limit}) should be less or equal than wait (#{wait}) for stable screenshots" unless stability_time_limit <= wait
25
+
13
26
  @comparison_options = comparison_options || Diff.default_options
14
- @screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), @comparison_options[:driver])
27
+
28
+ driver = Diff::Drivers.for(@comparison_options)
29
+ @screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), driver)
15
30
  end
16
31
 
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
32
+ # Takes a comparison screenshot ensuring page stability
33
+ #
34
+ # Attempts to take a stable screenshot of the page by comparing several screenshot attempts until the page stops updating
35
+ # or the `:wait` limit is reached. If unable to achieve a stable state within the time limit, it annotates the attempts
36
+ # to aid debugging.
37
+ #
38
+ # @param screenshot_path [String, Pathname] The path where the screenshot will be saved.
39
+ # @return [void]
40
+ # @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time.
20
41
  def take_comparison_screenshot(screenshot_path)
21
42
  new_screenshot_path = take_stable_screenshot(screenshot_path)
22
43
 
@@ -31,42 +52,47 @@ module Capybara
31
52
  end
32
53
 
33
54
  def take_stable_screenshot(screenshot_path)
55
+ screenshot_path = screenshot_path.is_a?(String) ? Pathname.new(screenshot_path) : screenshot_path
34
56
  # We try to compare first attempt with checkout version, in order to not run next screenshots
35
57
  attempt_path = nil
36
- screenshot_started_at = last_attempt_at = Time.now
58
+ deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
37
59
 
38
60
  # Cleanup all previous attempts for sure
39
61
  Screenshoter.cleanup_attempts_screenshots(screenshot_path)
40
62
 
41
63
  0.step do |i|
42
- # Prevents redundant screenshots generations
64
+ # FIXME: it should be wait, and wait should be replaced with stability_time_limit
43
65
  sleep(stability_time_limit) unless i == 0
66
+ attempt_path, prev_attempt_path = attempt_next_screenshot(attempt_path, i, screenshot_path)
67
+ return attempt_path if attempt_successful?(attempt_path, prev_attempt_path)
68
+ return nil if timeout?(deadline_at)
69
+ end
70
+ end
44
71
 
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)
72
+ private
55
73
 
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?
74
+ def attempt_successful?(attempt_path, prev_attempt_path)
75
+ return false unless prev_attempt_path
76
+ build_comparison_for(attempt_path, prev_attempt_path).quick_equal?
77
+ rescue ArgumentError
78
+ false
79
+ end
58
80
 
59
- # If timeout then we failed to generate valid screenshot
60
- return nil if timeout?(elapsed_time)
61
- end
81
+ def attempt_next_screenshot(prev_attempt_path, i, screenshot_path)
82
+ new_attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i)
83
+ @screenshoter.take_screenshot(new_attempt_path)
84
+ [new_attempt_path, prev_attempt_path]
62
85
  end
63
86
 
64
- private
87
+ def timeout?(deadline_at)
88
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
89
+ end
65
90
 
66
91
  def build_comparison_for(attempt_path, previous_attempt_path)
67
92
  ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options)
68
93
  end
69
94
 
95
+ # TODO: Move to the HistoricalReporter
70
96
  def annotate_attempts_and_fail!(screenshot_path)
71
97
  screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path)
72
98
 
@@ -84,22 +110,18 @@ module Capybara
84
110
  attempts_comparison = build_comparison_for(file_name, previous_file)
85
111
 
86
112
  if attempts_comparison.different?
87
- FileUtils.mv(attempts_comparison.annotated_base_image_path, previous_file, force: true)
113
+ FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true)
88
114
  else
89
115
  warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
90
- "#{previous_file} and #{file_name} are equal"
116
+ "#{previous_file} and #{file_name} are equal"
91
117
  end
92
118
 
93
- FileUtils.rm(attempts_comparison.annotated_image_path, force: true)
119
+ FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true)
94
120
  end
95
121
 
96
122
  previous_file = file_name
97
123
  end
98
124
  end
99
-
100
- def timeout?(elapsed_time)
101
- elapsed_time > wait
102
- end
103
125
  end
104
126
  end
105
127
  end