capybara-screenshot-diff 1.2.1 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,16 +3,16 @@
3
3
  module Capybara
4
4
  module Screenshot
5
5
  module Os
6
- ON_WINDOWS = !!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
7
- ON_MAC = !!(RbConfig::CONFIG['host_os'] =~ /darwin/)
8
- ON_LINUX = !!(RbConfig::CONFIG['host_os'] =~ /linux/)
6
+ ON_WINDOWS = !!(RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/)
7
+ ON_MAC = !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
8
+ ON_LINUX = !!(RbConfig::CONFIG["host_os"] =~ /linux/)
9
9
 
10
10
  def os_name
11
- return 'windows' if ON_WINDOWS
12
- return 'macos' if ON_MAC
13
- return 'linux' if ON_LINUX
11
+ return "windows" if ON_WINDOWS
12
+ return "macos" if ON_MAC
13
+ return "linux" if ON_LINUX
14
14
 
15
- 'unknown'
15
+ "unknown"
16
16
  end
17
17
  end
18
18
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'os'
3
+ require_relative "os"
4
4
 
5
5
  module Capybara
6
6
  module Screenshot
@@ -20,16 +20,23 @@ module Stabilization
20
20
  }()
21
21
  JS
22
22
 
23
- def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_limit:,
24
- area_size_limit:, skip_area:, stability_time_limit:)
25
- blurred_input = prepare_page_for_screenshot
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:)
26
34
  previous_file_name = comparison.old_file_name
27
35
  screenshot_started_at = last_image_change_at = Time.now
36
+ clean_stabilization_images(comparison.new_file_name)
37
+
28
38
  1.step do |i|
29
39
  take_right_size_screenshot(comparison)
30
-
31
- break unless stability_time_limit
32
-
33
40
  if comparison.quick_equal?
34
41
  clean_stabilization_images(comparison.new_file_name)
35
42
  break
@@ -37,10 +44,11 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
37
44
  comparison.reset
38
45
 
39
46
  if previous_file_name
40
- stabilization_comparison =
41
- ImageCompare.new(comparison.new_file_name, previous_file_name,
42
- color_distance_limit: color_distance_limit, shift_distance_limit: shift_distance_limit,
43
- area_size_limit: area_size_limit, skip_area: skip_area)
47
+ stabilization_comparison = make_stabilization_comparison_from(
48
+ comparison,
49
+ comparison.new_file_name,
50
+ previous_file_name
51
+ )
44
52
  if stabilization_comparison.quick_equal?
45
53
  if (Time.now - last_image_change_at) > stability_time_limit
46
54
  clean_stabilization_images(comparison.new_file_name)
@@ -52,45 +60,62 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
52
60
  end
53
61
  end
54
62
 
55
- previous_file_name = "#{comparison.new_file_name.chomp('.png')}_x#{format('%02i', i)}.png~"
63
+ previous_file_name = "#{comparison.new_file_name.chomp(".png")}" \
64
+ "_x#{format("%02i", i)}_#{(Time.now - screenshot_started_at).round(1)}s" \
65
+ "_#{stabilization_comparison.difference_region&.to_s&.gsub(", ", "_") || :initial}.png~"
56
66
  FileUtils.mv comparison.new_file_name, previous_file_name
57
67
 
58
- check_max_wait_time(comparison, screenshot_started_at,
59
- shift_distance_limit: shift_distance_limit)
68
+ check_max_wait_time(
69
+ comparison,
70
+ screenshot_started_at,
71
+ max_wait_time: max_wait_time(comparison.shift_distance_limit, wait)
72
+ )
73
+ end
74
+ end
75
+
76
+ def notice_how_to_avoid_this
77
+ unless @_csd_retina_warned
78
+ warn "Halving retina screenshot. " \
79
+ 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
80
+ @_csd_retina_warned = true
60
81
  end
61
- ensure
62
- blurred_input&.click
63
82
  end
64
83
 
65
84
  private
66
85
 
67
- def reduce_retina_image_size(file_name)
86
+ def make_stabilization_comparison_from(comparison, new_file_name, previous_file_name)
87
+ ImageCompare.new(new_file_name, previous_file_name, **comparison.driver_options)
88
+ end
89
+
90
+ def reduce_retina_image_size(file_name, driver)
68
91
  return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
69
92
 
70
- saved_image = ChunkyPNG::Image.from_file(file_name)
71
- width = Capybara::Screenshot.window_size[0]
72
- return if saved_image.width < width * 2
93
+ expected_image_width = Capybara::Screenshot.window_size[0]
94
+ saved_image = driver.from_file(file_name)
95
+ return if driver.width_for(saved_image) < expected_image_width * 2
73
96
 
74
- unless @_csd_retina_warned
75
- warn 'Halving retina screenshot. ' \
76
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
77
- @_csd_retina_warned = true
97
+ notice_how_to_avoid_this
98
+
99
+ new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
100
+ resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
101
+
102
+ Dir.mktmpdir do |dir|
103
+ resized_image_file = "#{dir}/resized.png"
104
+ driver.save_image_to(resized_image, resized_image_file)
105
+ FileUtils.mv(resized_image_file, file_name)
78
106
  end
79
- height = (width * saved_image.height) / saved_image.width
80
- resized_image = saved_image.resample_bilinear(width, height)
81
- resized_image.save(file_name)
82
107
  end
83
108
 
84
109
  def stabilization_images(base_file)
85
- Dir["#{base_file.chomp('.png')}_x*.png~"].sort
110
+ Dir["#{base_file.chomp(".png")}_x*.png~"].sort
86
111
  end
87
112
 
88
113
  def clean_stabilization_images(base_file)
89
114
  FileUtils.rm stabilization_images(base_file)
90
115
  end
91
116
 
92
- def prepare_page_for_screenshot
93
- assert_images_loaded
117
+ def prepare_page_for_screenshot(timeout:)
118
+ assert_images_loaded(timeout: timeout)
94
119
  if Capybara::Screenshot.blur_active_element
95
120
  active_element = execute_script(<<-JS)
96
121
  ae = document.activeElement;
@@ -102,7 +127,7 @@ def prepare_page_for_screenshot
102
127
  JS
103
128
  blurred_input = page.driver.send :unwrap_script_result, active_element
104
129
  end
105
- execute_script("$('*').css('caret-color','transparent')") if Capybara::Screenshot.hide_caret
130
+ execute_script(HIDE_CARET_SCRIPT) if Capybara::Screenshot.hide_caret
106
131
  blurred_input
107
132
  end
108
133
 
@@ -110,19 +135,43 @@ def take_right_size_screenshot(comparison)
110
135
  save_screenshot(comparison.new_file_name)
111
136
 
112
137
  # TODO(uwe): Remove when chromedriver takes right size screenshots
113
- reduce_retina_image_size(comparison.new_file_name)
138
+ reduce_retina_image_size(comparison.new_file_name, comparison.driver)
114
139
  # ODOT
115
140
  end
116
141
 
117
- def check_max_wait_time(comparison, screenshot_started_at, shift_distance_limit:)
142
+ def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
143
+ return if (Time.now - screenshot_started_at) < max_wait_time
144
+
145
+ annotate_stabilization_images(comparison)
146
+ # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
147
+ fail("Could not get stable screenshot within #{max_wait_time}s\n" \
148
+ "#{stabilization_images(comparison.new_file_name).join("\n")}")
149
+ end
150
+
151
+ def annotate_stabilization_images(comparison)
152
+ previous_file = comparison.old_file_name
153
+ stabilization_images(comparison.new_file_name).each do |file_name|
154
+ if File.exist? previous_file
155
+ stabilization_comparison = make_stabilization_comparison_from(
156
+ comparison,
157
+ file_name,
158
+ previous_file
159
+ )
160
+ if stabilization_comparison.different?
161
+ FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
162
+ end
163
+ FileUtils.rm stabilization_comparison.annotated_old_file_name
164
+ end
165
+ previous_file = file_name
166
+ end
167
+ end
168
+
169
+ def max_wait_time(shift_distance_limit, wait)
118
170
  shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
119
- max_wait_time = Capybara.default_max_wait_time * shift_factor
120
- assert((Time.now - screenshot_started_at) < max_wait_time,
121
- "Could not get stable screenshot within #{max_wait_time}s\n" \
122
- "#{stabilization_images(comparison.new_file_name).join("\n")}")
171
+ wait * shift_factor
123
172
  end
124
173
 
125
- def assert_images_loaded(timeout: Capybara.default_max_wait_time)
174
+ def assert_images_loaded(timeout:)
126
175
  return unless respond_to? :evaluate_script
127
176
 
128
177
  start = Time.now
@@ -130,8 +179,11 @@ def assert_images_loaded(timeout: Capybara.default_max_wait_time)
130
179
  pending_image = evaluate_script IMAGE_WAIT_SCRIPT
131
180
  break unless pending_image
132
181
 
133
- assert((Time.now - start) < timeout,
134
- "Images not loaded after #{timeout}s: #{pending_image.inspect}")
182
+ assert(
183
+ (Time.now - start) < timeout,
184
+ "Images not loaded after #{timeout}s: #{pending_image.inspect}"
185
+ )
186
+
135
187
  sleep 0.1
136
188
  end
137
189
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
- require 'capybara'
5
- require 'action_controller'
6
- require 'action_dispatch'
7
- require 'active_support/core_ext/string/strip'
8
- require_relative 'image_compare'
9
- require_relative 'stabilization'
10
- require_relative 'vcs'
3
+ require "English"
4
+ require "capybara"
5
+ require "action_controller"
6
+ require "action_dispatch"
7
+ require "active_support/core_ext/string/strip"
8
+ require_relative "image_compare"
9
+ require_relative "stabilization"
10
+ require_relative "vcs"
11
11
 
12
12
  # Add the `screenshot` method to ActionDispatch::IntegrationTest
13
13
  module Capybara
@@ -42,19 +42,13 @@ def screenshot_dir
42
42
  end
43
43
 
44
44
  def current_capybara_driver_class
45
- Capybara.drivers[Capybara.current_driver].call({}).class
45
+ Capybara.current_session.driver.class
46
46
  end
47
47
 
48
48
  def selenium?
49
49
  current_capybara_driver_class <= Capybara::Selenium::Driver
50
50
  end
51
51
 
52
- def poltergeist?
53
- return false unless defined?(Capybara::Poltergeist::Driver)
54
-
55
- current_capybara_driver_class <= Capybara::Poltergeist::Driver
56
- end
57
-
58
52
  def screenshot_section(name)
59
53
  @screenshot_section = name.to_s
60
54
  end
@@ -68,46 +62,59 @@ def screenshot_group(name)
68
62
  end
69
63
 
70
64
  # @return [Boolean] wether a screenshot was taken
71
- def screenshot(name, area_size_limit: Diff.area_size_limit,
65
+ def screenshot(
66
+ name,
67
+ stability_time_limit: Screenshot.stability_time_limit,
68
+ wait: Capybara.default_max_wait_time,
69
+ **driver_options
70
+ )
71
+ return false unless Screenshot.active?
72
+ return false if window_size_is_wrong?
73
+
74
+ driver_options = {
75
+ area_size_limit: Diff.area_size_limit,
72
76
  color_distance_limit: Diff.color_distance_limit,
73
- shift_distance_limit: Diff.shift_distance_limit, skip_area: Diff.skip_area,
74
- stability_time_limit: Screenshot.stability_time_limit)
75
- return unless Screenshot.active?
76
- return if window_size_is_wrong?
77
-
78
- skip_area = skip_area&.flatten&.each_cons(4)&.to_a # Allow nil or single or multiple areas
77
+ driver: Diff.driver,
78
+ shift_distance_limit: Diff.shift_distance_limit,
79
+ skip_area: Diff.skip_area,
80
+ tolerance: Diff.tolerance
81
+ }.merge(driver_options)
82
+
83
+ # Allow nil or single or multiple areas
84
+ if driver_options[:skip_area]
85
+ driver_options[:skip_area] = driver_options[:skip_area].compact.flatten&.each_cons(4)&.to_a
86
+ end
79
87
 
80
88
  if @screenshot_counter
81
- name = "#{format('%02i', @screenshot_counter)}_#{name}"
89
+ name = "#{format("%02i", @screenshot_counter)}_#{name}"
82
90
  @screenshot_counter += 1
83
91
  end
84
92
  name = full_name(name)
85
93
  file_name = "#{Screenshot.screenshot_area_abs}/#{name}.png"
86
94
 
87
95
  FileUtils.mkdir_p File.dirname(file_name)
88
- comparison = ImageCompare.new(file_name,
89
- dimensions: Screenshot.window_size, color_distance_limit: color_distance_limit,
90
- area_size_limit: area_size_limit, shift_distance_limit: shift_distance_limit,
91
- skip_area: skip_area)
96
+ comparison = ImageCompare.new(file_name, **driver_options)
92
97
  checkout_vcs(name, comparison)
93
- take_stable_screenshot(comparison, color_distance_limit: color_distance_limit,
94
- shift_distance_limit: shift_distance_limit,
95
- area_size_limit: area_size_limit,
96
- skip_area: skip_area,
97
- stability_time_limit: stability_time_limit)
98
- return unless comparison.old_file_exists?
98
+ begin
99
+ blurred_input = prepare_page_for_screenshot(timeout: wait)
100
+ if stability_time_limit
101
+ take_stable_screenshot(comparison, stability_time_limit: stability_time_limit, wait: wait)
102
+ else
103
+ take_right_size_screenshot(comparison)
104
+ end
105
+ ensure
106
+ blurred_input&.click
107
+ end
108
+
109
+ return false unless comparison.old_file_exists?
99
110
 
100
111
  (@test_screenshots ||= []) << [caller(1..1).first, name, comparison]
112
+
101
113
  true
102
114
  end
103
115
 
104
116
  def window_size_is_wrong?
105
117
  selenium? && Screenshot.window_size &&
106
-
107
- # FIXME(uwe): This happens with headless chrome. Why?!
108
- page.driver.browser.manage.window.size.width &&
109
- # EMXIF
110
-
111
118
  page.driver.browser.manage.window.size !=
112
119
  ::Selenium::WebDriver::Dimension.new(*Screenshot.window_size)
113
120
  end
@@ -115,21 +122,7 @@ def window_size_is_wrong?
115
122
  def assert_image_not_changed(caller, name, comparison)
116
123
  return unless comparison.different?
117
124
 
118
- # TODO(uwe): Remove check when we stop supporting Ruby 2.3 and older
119
- max_color_distance = if RUBY_VERSION >= '2.4'
120
- comparison.max_color_distance.ceil(1)
121
- else
122
- comparison.max_color_distance.ceil
123
- end
124
- # ODOT
125
-
126
- max_shift_distance = comparison.max_shift_distance
127
- "Screenshot does not match for '#{name}' (area: #{comparison.size}px #{comparison.dimensions}" \
128
- ", max_color_distance: #{max_color_distance}" \
129
- "#{", max_shift_distance: #{max_shift_distance}" if max_shift_distance})\n" \
130
- "#{comparison.new_file_name}\n#{comparison.annotated_old_file_name}\n" \
131
- "#{comparison.annotated_new_file_name}\n" \
132
- "at #{caller}"
125
+ "Screenshot does not match for '#{name}' #{comparison.error_message}\nat #{caller}"
133
126
  end
134
127
  end
135
128
  end
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'os'
3
+ require_relative "os"
4
4
  module Capybara
5
5
  module Screenshot
6
6
  module Diff
7
7
  module Vcs
8
- SILENCE_ERRORS = Os::ON_WINDOWS ? '2>nul' : '2>/dev/null'
8
+ SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null"
9
9
 
10
10
  def restore_git_revision(name, target_file_name)
11
11
  redirect_target = "#{target_file_name} #{SILENCE_ERRORS}"
12
- `git show HEAD~0:./#{Capybara::Screenshot.screenshot_area}/#{name}.png > #{redirect_target}`
12
+ show_command = "git show HEAD~0:./#{Capybara::Screenshot.screenshot_area}/#{name}.png"
13
+ if Capybara::Screenshot.use_lfs
14
+ `#{show_command} | git lfs smudge > #{redirect_target}`
15
+ else
16
+ `#{show_command} > #{redirect_target}`
17
+ end
13
18
  FileUtils.rm_f(target_file_name) unless $CHILD_STATUS == 0
14
19
  end
15
20
 
@@ -3,7 +3,7 @@
3
3
  module Capybara
4
4
  module Screenshot
5
5
  module Diff
6
- VERSION = '1.2.1'
6
+ VERSION = "1.4.2"
7
7
  end
8
8
  end
9
9
  end
@@ -1,31 +1,29 @@
1
1
  #!/usr/bin/env ruby -w
2
2
  # frozen_string_literal: true
3
3
 
4
- system('rubocop --auto-correct') || exit(1)
4
+ update_gemfiles = ARGV.delete("--update")
5
5
 
6
- update_gemfiles = ARGV.delete('--update')
7
-
8
- require 'yaml'
9
- travis = YAML.safe_load(File.read('.travis.yml'))
6
+ require "yaml"
7
+ travis = YAML.safe_load(File.read(".travis.yml"))
10
8
 
11
9
  def run_script(ruby, env, gemfile)
12
10
  env.scan(/\b(?<key>[A-Z_]+)="(?<value>.+?)"/) do |key, value|
13
11
  ENV[key] = value
14
12
  end
15
- puts '*' * 80
13
+ puts "*" * 80
16
14
  puts "Testing #{ruby} #{gemfile} #{env}"
17
15
  puts
18
16
  system("chruby-exec #{ruby} -- bundle exec rake") || exit(1)
19
17
  puts "Testing #{ruby} #{gemfile} OK"
20
- puts '*' * 80
18
+ puts "*" * 80
21
19
  end
22
20
 
23
21
  def use_gemfile(ruby, gemfile, update_gemfiles)
24
- puts '$' * 80
25
- ENV['BUNDLE_GEMFILE'] = gemfile
22
+ puts "$" * 80
23
+ ENV["BUNDLE_GEMFILE"] = gemfile
26
24
 
27
25
  bundler_version = `grep -A1 "BUNDLED WITH" #{gemfile}.lock | tail -n 1`
28
- bundler_version = '~> 2.0' if bundler_version.strip.empty?
26
+ bundler_version = "~> 2.0" if bundler_version.strip.empty?
29
27
 
30
28
  version_arg = "-v '#{bundler_version}'"
31
29
  bundler_gem_check_cmd = "chruby-exec #{ruby} -- gem query -i -n bundler #{version_arg} >/dev/null"
@@ -37,33 +35,33 @@ def use_gemfile(ruby, gemfile, update_gemfiles)
37
35
  system "chruby-exec #{ruby} -- bundle check >/dev/null || chruby-exec #{ruby} -- bundle install"
38
36
  end || exit(1)
39
37
  yield
40
- puts '$' * 80
38
+ puts "$" * 80
41
39
  end
42
40
 
43
- travis['rvm'].each do |ruby|
44
- next if ruby =~ /head/ # ruby-install does not support HEAD installation
41
+ travis["rvm"].each do |ruby|
42
+ next if /head/.match?(ruby) # ruby-install does not support HEAD installation
45
43
 
46
- puts '#' * 80
44
+ puts "#" * 80
47
45
  puts "Testing #{ruby}"
48
46
  puts
49
47
  system "ruby-install --no-reinstall #{ruby}" || exit(1)
50
- travis['gemfile'].each do |gemfile|
51
- if travis['matrix'] &&
52
- (travis['matrix']['exclude'].to_a + travis['matrix']['allow_failures'].to_a)
53
- .any? { |f| f['rvm'] == ruby && (f['gemfile'].nil? || f['gemfile'] == gemfile) }
54
- puts 'Skipping known failure.'
48
+ travis["gemfile"].each do |gemfile|
49
+ if travis["matrix"] &&
50
+ (travis["matrix"]["exclude"].to_a + travis["matrix"]["allow_failures"].to_a)
51
+ .any? { |f| f["rvm"] == ruby && (f["gemfile"].nil? || f["gemfile"] == gemfile) }
52
+ puts "Skipping known failure."
55
53
  next
56
54
  end
57
55
  use_gemfile(ruby, gemfile, update_gemfiles) do
58
- travis['env'].each do |env|
56
+ travis["env"].each do |env|
59
57
  run_script(ruby, env, gemfile)
60
58
  end
61
59
  end
62
60
  end
63
61
  puts "Testing #{ruby} OK"
64
- puts '#' * 80
62
+ puts "#" * 80
65
63
  end
66
64
 
67
65
  print "\033[0;32m"
68
- print ' TESTS PASSED OK!'
66
+ print " TESTS PASSED OK!"
69
67
  puts "\033[0m"