capybara-screenshot-diff 1.6.2 → 1.8.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +25 -0
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +65 -100
- 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 -210
- 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 +38 -35
- 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,210 +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
|
-
Dir.mktmpdir do |dir|
|
114
|
-
resized_image_file = "#{dir}/resized.png"
|
115
|
-
driver.save_image_to(resized_image, resized_image_file)
|
116
|
-
FileUtils.mv(resized_image_file, file_name)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def stabilization_images(base_file)
|
121
|
-
Dir["#{base_file.chomp(".png")}_x*.png#{ImageCompare::TMP_FILE_SUFFIX}"].sort
|
122
|
-
end
|
123
|
-
|
124
|
-
def clean_stabilization_images(base_file)
|
125
|
-
FileUtils.rm stabilization_images(base_file)
|
126
|
-
end
|
127
|
-
|
128
|
-
def prepare_page_for_screenshot(timeout:)
|
129
|
-
assert_images_loaded(timeout: timeout)
|
130
|
-
if Capybara::Screenshot.blur_active_element
|
131
|
-
active_element = execute_script(<<-JS)
|
132
|
-
ae = document.activeElement;
|
133
|
-
if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
|
134
|
-
ae.blur();
|
135
|
-
return ae;
|
136
|
-
}
|
137
|
-
return null;
|
138
|
-
JS
|
139
|
-
blurred_input = page.driver.send :unwrap_script_result, active_element
|
140
|
-
end
|
141
|
-
execute_script(HIDE_CARET_SCRIPT) if Capybara::Screenshot.hide_caret
|
142
|
-
blurred_input
|
143
|
-
end
|
144
|
-
|
145
|
-
def take_right_size_screenshot(comparison, crop:)
|
146
|
-
save_screenshot(comparison.new_file_name)
|
147
|
-
|
148
|
-
# TODO(uwe): Remove when chromedriver takes right size screenshots
|
149
|
-
reduce_retina_image_size(comparison.new_file_name, comparison.driver)
|
150
|
-
# ODOT
|
151
|
-
|
152
|
-
if crop
|
153
|
-
full_img = comparison.driver.from_file(comparison.new_file_name)
|
154
|
-
area_img = full_img.crop(crop[0], crop[1], crop[2] - crop[0], crop[3] - crop[1])
|
155
|
-
comparison.driver.save_image_to(area_img, comparison.new_file_name)
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
|
160
|
-
return if (Time.now - screenshot_started_at) < max_wait_time
|
161
|
-
|
162
|
-
annotate_stabilization_images(comparison)
|
163
|
-
# FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
|
164
|
-
fail("Could not get stable screenshot within #{max_wait_time}s\n" \
|
165
|
-
"#{stabilization_images(comparison.new_file_name).join("\n")}")
|
166
|
-
end
|
167
|
-
|
168
|
-
def annotate_stabilization_images(comparison)
|
169
|
-
previous_file = comparison.old_file_name
|
170
|
-
stabilization_images(comparison.new_file_name).each do |file_name|
|
171
|
-
if File.exist? previous_file
|
172
|
-
stabilization_comparison = make_stabilization_comparison_from(
|
173
|
-
comparison,
|
174
|
-
file_name,
|
175
|
-
previous_file
|
176
|
-
)
|
177
|
-
if stabilization_comparison.different?
|
178
|
-
FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
|
179
|
-
end
|
180
|
-
FileUtils.rm stabilization_comparison.annotated_old_file_name
|
181
|
-
end
|
182
|
-
previous_file = file_name
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def max_wait_time(shift_distance_limit, wait)
|
187
|
-
shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
|
188
|
-
wait * shift_factor
|
189
|
-
end
|
190
|
-
|
191
|
-
def assert_images_loaded(timeout:)
|
192
|
-
return unless respond_to? :evaluate_script
|
193
|
-
|
194
|
-
start = Time.now
|
195
|
-
loop do
|
196
|
-
pending_image = evaluate_script IMAGE_WAIT_SCRIPT
|
197
|
-
break unless pending_image
|
198
|
-
|
199
|
-
assert(
|
200
|
-
(Time.now - start) < timeout,
|
201
|
-
"Images not loaded after #{timeout}s: #{pending_image.inspect}"
|
202
|
-
)
|
203
|
-
|
204
|
-
sleep 0.1
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
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
|