capybara-screenshot-diff 1.8.3 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
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