capybara-screenshot-diff 1.7.0 → 1.8.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +29 -0
  3. data/capybara-screenshot-diff.gemspec +7 -4
  4. data/gems.rb +8 -2
  5. data/lib/capybara/screenshot/diff/browser_helpers.rb +29 -28
  6. data/lib/capybara/screenshot/diff/cucumber.rb +11 -0
  7. data/lib/capybara/screenshot/diff/difference.rb +63 -0
  8. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +42 -0
  9. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +188 -260
  10. data/lib/capybara/screenshot/diff/drivers/utils.rb +16 -0
  11. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +53 -103
  12. data/lib/capybara/screenshot/diff/drivers.rb +16 -0
  13. data/lib/capybara/screenshot/diff/image_compare.rb +125 -154
  14. data/lib/capybara/screenshot/diff/os.rb +1 -1
  15. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +128 -0
  16. data/lib/capybara/screenshot/diff/screenshoter.rb +137 -0
  17. data/lib/capybara/screenshot/diff/stabilization.rb +0 -184
  18. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +106 -0
  19. data/lib/capybara/screenshot/diff/test_methods.rb +51 -90
  20. data/lib/capybara/screenshot/diff/vcs.rb +44 -22
  21. data/lib/capybara/screenshot/diff/version.rb +1 -1
  22. data/lib/capybara/screenshot/diff.rb +13 -17
  23. data/sig/capybara/screenshot/diff/diff.rbs +28 -0
  24. data/sig/capybara/screenshot/diff/difference.rbs +33 -0
  25. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +63 -0
  26. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +36 -0
  27. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +89 -0
  28. data/sig/capybara/screenshot/diff/drivers/utils.rbs +13 -0
  29. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +25 -0
  30. data/sig/capybara/screenshot/diff/image_compare.rbs +93 -0
  31. data/sig/capybara/screenshot/diff/os.rbs +11 -0
  32. data/sig/capybara/screenshot/diff/region.rbs +43 -0
  33. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +60 -0
  34. data/sig/capybara/screenshot/diff/screenshoter.rbs +48 -0
  35. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +29 -0
  36. data/sig/capybara/screenshot/diff/test_methods.rbs +39 -0
  37. data/sig/capybara/screenshot/diff/vcs.rbs +17 -0
  38. metadata +36 -27
  39. data/.gitattributes +0 -4
  40. data/.github/dependabot.yml +0 -8
  41. data/.github/workflows/lint.yml +0 -25
  42. data/.github/workflows/test.yml +0 -138
  43. data/.gitignore +0 -14
  44. data/.standard.yml +0 -12
  45. data/CONTRIBUTING.md +0 -24
  46. data/Dockerfile +0 -59
  47. data/README.md +0 -567
  48. data/bin/bundle +0 -114
  49. data/bin/console +0 -15
  50. data/bin/install-vips +0 -11
  51. data/bin/rake +0 -27
  52. data/bin/setup +0 -8
  53. data/bin/standardrb +0 -29
  54. data/gemfiles/rails60_gems.rb +0 -8
  55. data/gemfiles/rails61_gems.rb +0 -7
  56. data/gemfiles/rails70_gems.rb +0 -7
  57. data/tmp/.keep +0 -0
@@ -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,137 @@
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
+ assert(
122
+ (Time.now - start) < timeout,
123
+ "Images not loaded after #{timeout}s: #{pending_image.inspect}"
124
+ )
125
+
126
+ sleep 0.025
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def selenium_with_retina_screen?
133
+ Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
134
+ end
135
+ end
136
+ end
137
+ end
@@ -1,184 +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
- def take_stable_screenshot(comparison, stability_time_limit:, wait:, crop:)
12
- previous_file_name = comparison.old_file_name
13
- screenshot_started_at = last_image_change_at = Time.now
14
- clean_stabilization_images(comparison.new_file_name)
15
-
16
- 1.step do |i|
17
- take_right_size_screenshot(comparison, crop: crop)
18
- if comparison.quick_equal?
19
- clean_stabilization_images(comparison.new_file_name)
20
- break
21
- end
22
-
23
- comparison.reset
24
-
25
- if previous_file_name
26
- stabilization_comparison = make_stabilization_comparison_from(
27
- comparison,
28
- comparison.new_file_name,
29
- previous_file_name
30
- )
31
- if stabilization_comparison.quick_equal?
32
- if (Time.now - last_image_change_at) > stability_time_limit
33
- clean_stabilization_images(comparison.new_file_name)
34
- break
35
- end
36
- next
37
- else
38
- last_image_change_at = Time.now
39
- end
40
- end
41
-
42
- previous_file_name = build_snapshot_version_file_name(
43
- comparison,
44
- i,
45
- screenshot_started_at,
46
- stabilization_comparison
47
- )
48
-
49
- FileUtils.mv(comparison.new_file_name, previous_file_name)
50
-
51
- check_max_wait_time(
52
- comparison,
53
- screenshot_started_at,
54
- max_wait_time: max_wait_time(comparison.shift_distance_limit, wait)
55
- )
56
- end
57
- end
58
-
59
- def notice_how_to_avoid_this
60
- unless defined?(@_csd_retina_warned)
61
- warn "Halving retina screenshot. " \
62
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
63
- @_csd_retina_warned = true
64
- end
65
- end
66
-
67
- private
68
-
69
- def build_snapshot_version_file_name(comparison, iteration, screenshot_started_at, stabilization_comparison)
70
- "#{comparison.new_file_name.chomp(".png")}" \
71
- "_x#{format("%02i", iteration)}_#{(Time.now - screenshot_started_at).round(1)}s" \
72
- "_#{stabilization_comparison.difference_coordinates&.to_s&.gsub(", ", "_") || :initial}.png" \
73
- "#{ImageCompare::TMP_FILE_SUFFIX}"
74
- end
75
-
76
- def make_stabilization_comparison_from(comparison, new_file_name, previous_file_name)
77
- ImageCompare.new(new_file_name, previous_file_name, comparison.driver_options)
78
- end
79
-
80
- def reduce_retina_image_size(file_name, driver)
81
- return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
82
-
83
- expected_image_width = Capybara::Screenshot.window_size[0]
84
- saved_image = driver.from_file(file_name)
85
- return if driver.width_for(saved_image) < expected_image_width * 2
86
-
87
- notice_how_to_avoid_this
88
-
89
- new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
90
- resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
91
-
92
- driver.save_image_to(resized_image, file_name)
93
- end
94
-
95
- def stabilization_images(base_file)
96
- Dir["#{base_file.chomp(".png")}_x*.png#{ImageCompare::TMP_FILE_SUFFIX}"].sort
97
- end
98
-
99
- def clean_stabilization_images(base_file)
100
- FileUtils.rm stabilization_images(base_file)
101
- end
102
-
103
- def prepare_page_for_screenshot(timeout:)
104
- assert_images_loaded(timeout: timeout)
105
-
106
- if Capybara::Screenshot.blur_active_element
107
- blurred_input = blur_from_focused_element
108
- end
109
-
110
- if Capybara::Screenshot.hide_caret
111
- hide_caret
112
- end
113
-
114
- blurred_input
115
- end
116
-
117
- def take_right_size_screenshot(comparison, crop:)
118
- driver = comparison.driver
119
-
120
- save_screenshot(comparison.new_file_name)
121
-
122
- # TODO(uwe): Remove when chromedriver takes right size screenshots
123
- reduce_retina_image_size(comparison.new_file_name, driver)
124
- # ODOT
125
-
126
- if crop
127
- image = driver.from_file(comparison.new_file_name)
128
- cropped_image = driver.crop(crop, image)
129
- driver.save_image_to(cropped_image, comparison.new_file_name)
130
- end
131
- end
132
-
133
- def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
134
- return if (Time.now - screenshot_started_at) < max_wait_time
135
-
136
- annotate_stabilization_images(comparison)
137
- # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
138
- fail("Could not get stable screenshot within #{max_wait_time}s\n" \
139
- "#{stabilization_images(comparison.new_file_name).join("\n")}")
140
- end
141
-
142
- def annotate_stabilization_images(comparison)
143
- previous_file = comparison.old_file_name
144
- stabilization_images(comparison.new_file_name).each do |file_name|
145
- if File.exist? previous_file
146
- stabilization_comparison = make_stabilization_comparison_from(
147
- comparison,
148
- file_name,
149
- previous_file
150
- )
151
- if stabilization_comparison.different?
152
- FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
153
- end
154
- FileUtils.rm stabilization_comparison.annotated_old_file_name
155
- end
156
- previous_file = file_name
157
- end
158
- end
159
-
160
- def max_wait_time(shift_distance_limit, wait)
161
- shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
162
- wait * shift_factor
163
- end
164
-
165
- def assert_images_loaded(timeout:)
166
- return unless respond_to? :evaluate_script
167
-
168
- start = Time.now
169
- loop do
170
- pending_image = pending_image_to_load
171
- break unless pending_image
172
-
173
- assert(
174
- (Time.now - start) < timeout,
175
- "Images not loaded after #{timeout}s: #{pending_image.inspect}"
176
- )
177
-
178
- sleep 0.1
179
- end
180
- end
181
- end
182
- end
183
- end
184
- 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