capybara-screenshot-diff 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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:, wait:)
25
- blurred_input = prepare_page_for_screenshot(timeout: wait)
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,37 +60,54 @@ 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
- wait: wait, 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)
@@ -102,7 +127,7 @@ def prepare_page_for_screenshot(timeout:)
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,16 +135,40 @@ 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, wait:, 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 = wait * 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
174
  def assert_images_loaded(timeout:)
@@ -130,8 +179,11 @@ def assert_images_loaded(timeout:)
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,48 +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
- wait: Capybara.default_max_wait_time)
76
- return unless Screenshot.active?
77
- return if window_size_is_wrong?
78
-
79
- 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
80
87
 
81
88
  if @screenshot_counter
82
- name = "#{format('%02i', @screenshot_counter)}_#{name}"
89
+ name = "#{format("%02i", @screenshot_counter)}_#{name}"
83
90
  @screenshot_counter += 1
84
91
  end
85
92
  name = full_name(name)
86
93
  file_name = "#{Screenshot.screenshot_area_abs}/#{name}.png"
87
94
 
88
95
  FileUtils.mkdir_p File.dirname(file_name)
89
- comparison = ImageCompare.new(file_name,
90
- dimensions: Screenshot.window_size, color_distance_limit: color_distance_limit,
91
- area_size_limit: area_size_limit, shift_distance_limit: shift_distance_limit,
92
- skip_area: skip_area)
96
+ comparison = ImageCompare.new(file_name, **driver_options)
93
97
  checkout_vcs(name, comparison)
94
- take_stable_screenshot(comparison, color_distance_limit: color_distance_limit,
95
- shift_distance_limit: shift_distance_limit,
96
- area_size_limit: area_size_limit,
97
- skip_area: skip_area,
98
- stability_time_limit: stability_time_limit,
99
- wait: wait)
100
- 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?
101
110
 
102
111
  (@test_screenshots ||= []) << [caller(1..1).first, name, comparison]
112
+
103
113
  true
104
114
  end
105
115
 
106
116
  def window_size_is_wrong?
107
117
  selenium? && Screenshot.window_size &&
108
-
109
- # FIXME(uwe): This happens with headless chrome. Why?!
110
- page.driver.browser.manage.window.size.width &&
111
- # EMXIF
112
-
113
118
  page.driver.browser.manage.window.size !=
114
119
  ::Selenium::WebDriver::Dimension.new(*Screenshot.window_size)
115
120
  end
@@ -117,21 +122,7 @@ def window_size_is_wrong?
117
122
  def assert_image_not_changed(caller, name, comparison)
118
123
  return unless comparison.different?
119
124
 
120
- # TODO(uwe): Remove check when we stop supporting Ruby 2.3 and older
121
- max_color_distance = if RUBY_VERSION >= '2.4'
122
- comparison.max_color_distance.ceil(1)
123
- else
124
- comparison.max_color_distance.ceil
125
- end
126
- # ODOT
127
-
128
- max_shift_distance = comparison.max_shift_distance
129
- "Screenshot does not match for '#{name}' (area: #{comparison.size}px #{comparison.dimensions}" \
130
- ", max_color_distance: #{max_color_distance}" \
131
- "#{", max_shift_distance: #{max_shift_distance}" if max_shift_distance})\n" \
132
- "#{comparison.new_file_name}\n#{comparison.annotated_old_file_name}\n" \
133
- "#{comparison.annotated_new_file_name}\n" \
134
- "at #{caller}"
125
+ "Screenshot does not match for '#{name}' #{comparison.error_message}\nat #{caller}"
135
126
  end
136
127
  end
137
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.3.0'
6
+ VERSION = "1.5.0"
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"