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