capybara-screenshot-diff 1.8.3 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -11
  3. data/capybara-screenshot-diff.gemspec +2 -3
  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 +5 -5
  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 -5
  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 +36 -74
  18. data/lib/capybara/screenshot/diff/screenshoter.rb +46 -54
  19. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +65 -63
  20. data/lib/capybara/screenshot/diff/test_methods.rb +76 -9
  21. data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +0 -7
  22. data/lib/capybara/screenshot/diff/vcs.rb +25 -23
  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/attempts_reporter.rb +49 -0
  27. data/lib/capybara_screenshot_diff/cucumber.rb +12 -0
  28. data/lib/capybara_screenshot_diff/dsl.rb +11 -0
  29. data/lib/capybara_screenshot_diff/minitest.rb +45 -0
  30. data/lib/capybara_screenshot_diff/rspec.rb +32 -0
  31. data/lib/capybara_screenshot_diff/snap.rb +55 -0
  32. data/lib/capybara_screenshot_diff/snap_manager.rb +76 -0
  33. data/lib/capybara_screenshot_diff.rb +86 -0
  34. metadata +20 -39
  35. data/lib/capybara/screenshot/diff/stabilization.rb +0 -0
  36. data/sig/capybara/screenshot/diff/diff.rbs +0 -28
  37. data/sig/capybara/screenshot/diff/difference.rbs +0 -33
  38. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +0 -63
  39. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +0 -36
  40. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +0 -89
  41. data/sig/capybara/screenshot/diff/drivers/utils.rbs +0 -13
  42. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +0 -25
  43. data/sig/capybara/screenshot/diff/image_compare.rbs +0 -93
  44. data/sig/capybara/screenshot/diff/os.rbs +0 -11
  45. data/sig/capybara/screenshot/diff/region.rbs +0 -43
  46. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +0 -60
  47. data/sig/capybara/screenshot/diff/screenshoter.rbs +0 -48
  48. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +0 -29
  49. data/sig/capybara/screenshot/diff/test_methods.rbs +0 -39
  50. 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
@@ -1,94 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "capybara_screenshot_diff/snap_manager"
3
4
  require_relative "screenshoter"
4
5
  require_relative "stable_screenshoter"
5
6
  require_relative "browser_helpers"
6
7
  require_relative "vcs"
8
+ require_relative "area_calculator"
7
9
 
8
10
  module Capybara
9
11
  module Screenshot
10
12
  module Diff
11
13
  class ScreenshotMatcher
12
- attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path
14
+ attr_reader :screenshot_full_name, :driver_options, :screenshot_format
13
15
 
14
16
  def initialize(screenshot_full_name, options = {})
15
17
  @screenshot_full_name = screenshot_full_name
16
18
  @driver_options = Diff.default_options.merge(options)
17
19
 
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
+ @screenshot_format = @driver_options[:screenshot_format]
21
+ @snapshot = CapybaraScreenshotDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format)
20
22
  end
21
23
 
22
24
  def build_screenshot_matches_job
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
+ driver_options[:driver] = Drivers.for(driver_options[:driver])
38
36
 
39
- create_output_directory_for(screenshot_path) unless screenshot_path.exist?
37
+ @snapshot.checkout_base_screenshot
40
38
 
41
- checkout_base_screenshot
39
+ # When fail_if_new is true no need to create screenshot if base screenshot is missing
40
+ return if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist?
42
41
 
43
- capture_options = {
44
- crop: crop,
45
- stability_time_limit: driver_options.delete(:stability_time_limit),
46
- wait: driver_options.delete(:wait)
47
- }
42
+ capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
48
43
 
49
- take_comparison_screenshot(capture_options, driver_options, screenshot_path)
44
+ # Load new screenshot from Browser
45
+ take_comparison_screenshot(capture_options, comparison_options, @snapshot)
50
46
 
51
- return unless base_screenshot_path.exist?
47
+ # Pre-computation: No need to compare without base screenshot
48
+ return unless @snapshot.base_path.exist?
52
49
 
53
50
  # 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")
51
+ [screenshot_full_name, ImageCompare.new(@snapshot.path, @snapshot.base_path, comparison_options)]
66
52
  end
67
53
 
68
54
  private
69
55
 
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
56
+ def extract_capture_and_comparison_options!(driver_options = {})
57
+ [
58
+ {
59
+ # screenshot options
60
+ capybara_screenshot_options: driver_options[:capybara_screenshot_options],
61
+ crop: driver_options.delete(:crop),
62
+ # delivery options
63
+ screenshot_format: driver_options[:screenshot_format],
64
+ # stability options
65
+ stability_time_limit: driver_options.delete(:stability_time_limit),
66
+ wait: driver_options.delete(:wait)
67
+ },
68
+ driver_options
69
+ ]
84
70
  end
85
71
 
86
72
  # Try to get screenshot from browser.
87
73
  # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
88
74
  # 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)
75
+ def take_comparison_screenshot(capture_options, comparison_options, snapshot = nil)
76
+ screenshoter = build_screenshoter_for(capture_options, comparison_options)
77
+ screenshoter.take_comparison_screenshot(snapshot)
92
78
  end
93
79
 
94
80
  def build_screenshoter_for(capture_options, comparison_options = {})
@@ -98,30 +84,6 @@ module Capybara
98
84
  Diff.screenshoter.new(capture_options, comparison_options[:driver])
99
85
  end
100
86
  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
87
  end
126
88
  end
127
89
  end
@@ -6,11 +6,10 @@ require_relative "browser_helpers"
6
6
  module Capybara
7
7
  module Screenshot
8
8
  class Screenshoter
9
- attr_reader :capture_options, :comparison_options, :driver
9
+ attr_reader :capture_options, :driver
10
10
 
11
11
  def initialize(capture_options, driver)
12
12
  @capture_options = capture_options
13
- @comparison_options = comparison_options
14
13
  @driver = driver
15
14
  end
16
15
 
@@ -22,88 +21,54 @@ module Capybara
22
21
  @capture_options[:wait]
23
22
  end
24
23
 
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)
24
+ def capybara_screenshot_options
25
+ @capture_options[:capybara_screenshot_options] || {}
31
26
  end
32
27
 
33
28
  # Try to get screenshot from browser.
34
29
  # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
35
30
  # 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)
31
+ def take_comparison_screenshot(snapshot)
32
+ capture_screenshot_at(snapshot)
33
+ snapshot.cleanup_attempts
43
34
  end
44
35
 
45
- def self.gen_next_attempt_path(screenshot_path, iteration)
46
- Pathname.new(screenshot_path).sub_ext(format(".attempt_%02i.png", iteration))
47
- end
36
+ PNG_EXTENSION = ".png"
48
37
 
49
38
  def take_screenshot(screenshot_path)
50
39
  blurred_input = prepare_page_for_screenshot(timeout: wait)
51
40
 
52
41
  # Take browser screenshot and save
53
- browser_save_screenshot(screenshot_path)
42
+ save_and_process_screenshot(screenshot_path)
54
43
 
55
- # Load saved screenshot and pre-process it
56
- process_screenshot(screenshot_path)
57
- ensure
58
44
  blurred_input&.click
59
45
  end
60
46
 
61
- def browser_save_screenshot(screenshot_path)
62
- BrowserHelpers.session.save_screenshot(screenshot_path)
63
- end
47
+ def process_screenshot(stored_path, screenshot_path)
48
+ screenshot_image = driver.from_file(stored_path)
64
49
 
65
- def process_screenshot(screenshot_path)
66
50
  # TODO(uwe): Remove when chromedriver takes right size screenshots
67
51
  # TODO: Adds tests when this case is true
68
- if selenium_with_retina_screen?
69
- reduce_retina_image_size(screenshot_path)
70
- end
52
+ screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
71
53
  # ODOT
72
54
 
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)
55
+ screenshot_image = driver.crop(crop, screenshot_image) if crop
89
56
 
90
- driver.save_image_to(resized_image, file_name)
57
+ driver.save_image_to(screenshot_image, screenshot_path)
91
58
  end
92
59
 
93
60
  def notice_how_to_avoid_this
94
61
  unless defined?(@_csd_retina_warned)
95
62
  warn "Halving retina screenshot. " \
96
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
63
+ 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
97
64
  @_csd_retina_warned = true
98
65
  end
99
66
  end
100
67
 
101
68
  def prepare_page_for_screenshot(timeout:)
102
- wait_images_loaded(timeout: timeout)
69
+ wait_images_loaded(timeout: timeout) if timeout
103
70
 
104
- blurred_input = if Screenshot.blur_active_element
105
- BrowserHelpers.blur_from_focused_element
106
- end
71
+ blurred_input = BrowserHelpers.blur_from_focused_element if Screenshot.blur_active_element
107
72
 
108
73
  if Screenshot.hide_caret
109
74
  BrowserHelpers.hide_caret
@@ -113,13 +78,15 @@ module Capybara
113
78
  end
114
79
 
115
80
  def wait_images_loaded(timeout:)
116
- start = Time.now
81
+ return unless timeout
82
+
83
+ deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
117
84
  loop do
118
85
  pending_image = BrowserHelpers.pending_image_to_load
119
86
  break unless pending_image
120
87
 
121
- if (Time.now - start) >= timeout
122
- raise Capybara::Screenshot::Diff::ASSERTION, "Images not loaded after #{timeout}s: #{pending_image.inspect}"
88
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
89
+ raise CapybaraScreenshotDiff::ExpectationNotMet, "Images have not been loaded after #{timeout}s: #{pending_image.inspect}"
123
90
  end
124
91
 
125
92
  sleep 0.025
@@ -128,6 +95,31 @@ module Capybara
128
95
 
129
96
  private
130
97
 
98
+ def save_and_process_screenshot(screenshot_path)
99
+ tmpfile = Tempfile.new([screenshot_path.basename.to_s, PNG_EXTENSION])
100
+ BrowserHelpers.session.save_screenshot(tmpfile.path, **capybara_screenshot_options)
101
+ # Load saved screenshot and pre-process it
102
+ process_screenshot(tmpfile.path, screenshot_path)
103
+ ensure
104
+ File.unlink(tmpfile) if tmpfile
105
+ end
106
+
107
+ def capture_screenshot_at(snapshot)
108
+ take_screenshot(snapshot.next_attempt_path!)
109
+
110
+ snapshot.commit_last_attempt
111
+ end
112
+
113
+ def resize_if_needed(saved_image)
114
+ expected_image_width = Screenshot.window_size[0]
115
+ return saved_image if driver.width_for(saved_image) < expected_image_width * 2
116
+
117
+ notice_how_to_avoid_this
118
+
119
+ new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
120
+ driver.resize_image_to(saved_image, expected_image_width, new_height)
121
+ end
122
+
131
123
  def selenium_with_retina_screen?
132
124
  Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
133
125
  end
@@ -8,97 +8,99 @@ module Capybara
8
8
 
9
9
  attr_reader :stability_time_limit, :wait
10
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])
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`.
19
+ def initialize(capture_options, comparison_options = {})
20
+ @stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS)
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
+
26
+ @comparison_options = comparison_options
27
+
28
+ driver = Diff::Drivers.for(@comparison_options)
29
+ @screenshoter = Diff.screenshoter.new(capture_options.except(:stability_time_limit), 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
20
- def take_comparison_screenshot(screenshot_path)
21
- new_screenshot_path = take_stable_screenshot(screenshot_path)
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 snapshot Snap The snapshot details to take a stable screenshot of.
39
+ # @return [void]
40
+ # @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time.
41
+ def take_comparison_screenshot(snapshot)
42
+ result = take_stable_screenshot(snapshot)
22
43
 
23
44
  # We failed to get stable browser state! Generate difference between attempts to overview moving parts!
24
- unless new_screenshot_path
45
+ unless result
25
46
  # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
26
- annotate_attempts_and_fail!(screenshot_path)
47
+ annotate_attempts_and_fail!(snapshot)
27
48
  end
28
49
 
29
- FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
30
- Screenshoter.cleanup_attempts_screenshots(screenshot_path)
50
+ # store success attempt as actual screenshot
51
+ snapshot.commit_last_attempt
52
+
53
+ # cleanup all previous attempts
54
+ snapshot.cleanup_attempts
31
55
  end
32
56
 
33
- def take_stable_screenshot(screenshot_path)
57
+ def take_stable_screenshot(snapshot)
34
58
  # 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
59
+ deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
37
60
 
38
61
  # Cleanup all previous attempts for sure
39
- Screenshoter.cleanup_attempts_screenshots(screenshot_path)
62
+ snapshot.cleanup_attempts
40
63
 
41
64
  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
65
+ # FIXME: it should be wait, and wait should be replaced with stability_time_limit
66
+ sleep(stability_time_limit) unless i == 0 # test prev_attempt_path is nil
52
67
 
53
- next unless prev_attempt_path
54
- stabilization_comparator = build_comparison_for(attempt_path, prev_attempt_path)
68
+ attempt_next_screenshot(snapshot)
55
69
 
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)
70
+ return true if attempt_successful?(snapshot)
71
+ return false if timeout?(deadline_at)
61
72
  end
62
73
  end
63
74
 
64
75
  private
65
76
 
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)
77
+ def attempt_successful?(snapshot)
78
+ return false unless snapshot.prev_attempt_path
74
79
 
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")}")
80
+ build_last_attempts_comparison_for(snapshot).quick_equal?
81
+ rescue ArgumentError
82
+ false
77
83
  end
78
84
 
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
85
+ def attempt_next_screenshot(snapshot)
86
+ @screenshoter.take_screenshot(snapshot.next_attempt_path!)
87
+ end
92
88
 
93
- FileUtils.rm(attempts_comparison.annotated_image_path, force: true)
94
- end
89
+ def timeout?(deadline_at)
90
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
91
+ end
95
92
 
96
- previous_file = file_name
97
- end
93
+ def build_last_attempts_comparison_for(snapshot)
94
+ ImageCompare.new(snapshot.attempt_path, snapshot.prev_attempt_path, @comparison_options)
98
95
  end
99
96
 
100
- def timeout?(elapsed_time)
101
- elapsed_time > wait
97
+ # TODO: Move to the HistoricalReporter
98
+ def annotate_attempts_and_fail!(snapshot)
99
+ require "capybara_screenshot_diff/attempts_reporter"
100
+ attempts_reporter = CapybaraScreenshotDiff::AttemptsReporter.new(snapshot, @comparison_options, {wait: wait, stability_time_limit: stability_time_limit})
101
+
102
+ # TODO: Move fail to the queue after tests passed
103
+ fail(attempts_reporter.generate)
102
104
  end
103
105
  end
104
106
  end