capybara-screenshot-diff 1.1.0 → 1.4.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,13 @@ 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:)
25
- blurred_input = prepare_page_for_screenshot
23
+ def take_stable_screenshot(comparison, stability_time_limit:, wait:)
26
24
  previous_file_name = comparison.old_file_name
27
25
  screenshot_started_at = last_image_change_at = Time.now
26
+ clean_stabilization_images(comparison.new_file_name)
27
+
28
28
  1.step do |i|
29
29
  take_right_size_screenshot(comparison)
30
-
31
- break unless Capybara::Screenshot.stability_time_limit
32
-
33
30
  if comparison.quick_equal?
34
31
  clean_stabilization_images(comparison.new_file_name)
35
32
  break
@@ -37,12 +34,13 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
37
34
  comparison.reset
38
35
 
39
36
  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)
37
+ stabilization_comparison = make_stabilization_comparison_from(
38
+ comparison,
39
+ comparison.new_file_name,
40
+ previous_file_name
41
+ )
44
42
  if stabilization_comparison.quick_equal?
45
- if (Time.now - last_image_change_at) > Capybara::Screenshot.stability_time_limit
43
+ if (Time.now - last_image_change_at) > stability_time_limit
46
44
  clean_stabilization_images(comparison.new_file_name)
47
45
  break
48
46
  end
@@ -50,48 +48,64 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
50
48
  else
51
49
  last_image_change_at = Time.now
52
50
  end
53
-
54
- check_max_wait_time(comparison, screenshot_started_at,
55
- shift_distance_limit: shift_distance_limit)
56
51
  end
57
52
 
58
- previous_file_name = "#{comparison.new_file_name.chomp('.png')}_x#{format('%02i', i)}.png~"
59
-
53
+ previous_file_name = "#{comparison.new_file_name.chomp(".png")}" \
54
+ "_x#{format("%02i", i)}_#{(Time.now - screenshot_started_at).round(1)}s" \
55
+ "_#{stabilization_comparison.difference_region&.to_s&.gsub(", ", "_") || :initial}.png~"
60
56
  FileUtils.mv comparison.new_file_name, previous_file_name
57
+
58
+ check_max_wait_time(
59
+ comparison,
60
+ screenshot_started_at,
61
+ max_wait_time: max_wait_time(comparison.shift_distance_limit, wait)
62
+ )
63
+ end
64
+ end
65
+
66
+ def notice_how_to_avoid_this
67
+ unless @_csd_retina_warned
68
+ warn "Halving retina screenshot. " \
69
+ 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
70
+ @_csd_retina_warned = true
61
71
  end
62
- ensure
63
- blurred_input&.click
64
72
  end
65
73
 
66
74
  private
67
75
 
68
- def reduce_retina_image_size(file_name)
76
+ def make_stabilization_comparison_from(comparison, new_file_name, previous_file_name)
77
+ ImageCompare.new(new_file_name, previous_file_name, **comparison.driver_options)
78
+ end
79
+
80
+ def reduce_retina_image_size(file_name, driver)
69
81
  return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
70
82
 
71
- saved_image = ChunkyPNG::Image.from_file(file_name)
72
- width = Capybara::Screenshot.window_size[0]
73
- return if saved_image.width < width * 2
83
+ expected_image_width = Capybara::Screenshot.window_size[0]
84
+ saved_image = driver.from_file(file_name)
85
+ return if driver.width_for(saved_image) < expected_image_width * 2
74
86
 
75
- unless @_csd_retina_warned
76
- warn 'Halving retina screenshot. ' \
77
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
78
- @_csd_retina_warned = true
87
+ notice_how_to_avoid_this
88
+
89
+ new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
90
+ resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
91
+
92
+ Dir.mktmpdir do |dir|
93
+ resized_image_file = "#{dir}/resized.png"
94
+ driver.save_image_to(resized_image, resized_image_file)
95
+ FileUtils.mv(resized_image_file, file_name)
79
96
  end
80
- height = (width * saved_image.height) / saved_image.width
81
- resized_image = saved_image.resample_bilinear(width, height)
82
- resized_image.save(file_name)
83
97
  end
84
98
 
85
99
  def stabilization_images(base_file)
86
- Dir["#{base_file.chomp('.png')}_x*.png~"].sort
100
+ Dir["#{base_file.chomp(".png")}_x*.png~"].sort
87
101
  end
88
102
 
89
103
  def clean_stabilization_images(base_file)
90
104
  FileUtils.rm stabilization_images(base_file)
91
105
  end
92
106
 
93
- def prepare_page_for_screenshot
94
- assert_images_loaded
107
+ def prepare_page_for_screenshot(timeout:)
108
+ assert_images_loaded(timeout: timeout)
95
109
  if Capybara::Screenshot.blur_active_element
96
110
  active_element = execute_script(<<-JS)
97
111
  ae = document.activeElement;
@@ -103,7 +117,15 @@ def prepare_page_for_screenshot
103
117
  JS
104
118
  blurred_input = page.driver.send :unwrap_script_result, active_element
105
119
  end
106
- execute_script("$('*').css('caret-color','transparent')") if Capybara::Screenshot.hide_caret
120
+ if Capybara::Screenshot.hide_caret && !@hid_caret
121
+ execute_script(<<~JS)
122
+ var style = document.createElement('style');
123
+ document.head.appendChild(style);
124
+ var styleSheet = style.sheet;
125
+ styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
126
+ JS
127
+ @hid_caret = true
128
+ end
107
129
  blurred_input
108
130
  end
109
131
 
@@ -111,19 +133,43 @@ def take_right_size_screenshot(comparison)
111
133
  save_screenshot(comparison.new_file_name)
112
134
 
113
135
  # TODO(uwe): Remove when chromedriver takes right size screenshots
114
- reduce_retina_image_size(comparison.new_file_name)
136
+ reduce_retina_image_size(comparison.new_file_name, comparison.driver)
115
137
  # ODOT
116
138
  end
117
139
 
118
- def check_max_wait_time(comparison, screenshot_started_at, shift_distance_limit:)
140
+ def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
141
+ return if (Time.now - screenshot_started_at) < max_wait_time
142
+
143
+ annotate_stabilization_images(comparison)
144
+ # FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
145
+ fail("Could not get stable screenshot within #{max_wait_time}s\n" \
146
+ "#{stabilization_images(comparison.new_file_name).join("\n")}")
147
+ end
148
+
149
+ def annotate_stabilization_images(comparison)
150
+ previous_file = comparison.old_file_name
151
+ stabilization_images(comparison.new_file_name).each do |file_name|
152
+ if File.exist? previous_file
153
+ stabilization_comparison = make_stabilization_comparison_from(
154
+ comparison,
155
+ file_name,
156
+ previous_file
157
+ )
158
+ if stabilization_comparison.different?
159
+ FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
160
+ end
161
+ FileUtils.rm stabilization_comparison.annotated_old_file_name
162
+ end
163
+ previous_file = file_name
164
+ end
165
+ end
166
+
167
+ def max_wait_time(shift_distance_limit, wait)
119
168
  shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
120
- max_wait_time = Capybara.default_max_wait_time * shift_factor
121
- assert((Time.now - screenshot_started_at) < max_wait_time,
122
- "Could not get stable screenshot within #{max_wait_time}s\n" \
123
- "#{stabilization_images(comparison.new_file_name).join("\n")}")
169
+ wait * shift_factor
124
170
  end
125
171
 
126
- def assert_images_loaded(timeout: Capybara.default_max_wait_time)
172
+ def assert_images_loaded(timeout:)
127
173
  return unless respond_to? :evaluate_script
128
174
 
129
175
  start = Time.now
@@ -131,8 +177,11 @@ def assert_images_loaded(timeout: Capybara.default_max_wait_time)
131
177
  pending_image = evaluate_script IMAGE_WAIT_SCRIPT
132
178
  break unless pending_image
133
179
 
134
- assert((Time.now - start) < timeout,
135
- "Images not loaded after #{timeout}s: #{pending_image.inspect}")
180
+ assert(
181
+ (Time.now - start) < timeout,
182
+ "Images not loaded after #{timeout}s: #{pending_image.inspect}"
183
+ )
184
+
136
185
  sleep 0.1
137
186
  end
138
187
  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,44 +62,59 @@ def screenshot_group(name)
68
62
  end
69
63
 
70
64
  # @return [Boolean] wether a screenshot was taken
71
- def screenshot(name, color_distance_limit: Diff.color_distance_limit,
72
- shift_distance_limit: Diff.shift_distance_limit, area_size_limit: Diff.area_size_limit,
73
- skip_area: Diff.skip_area)
74
- return unless Screenshot.active?
75
- return if window_size_is_wrong?
76
-
77
- skip_area = skip_area&.flatten&.each_cons(4)&.to_a # Allow nil or single or multiple areas
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,
76
+ color_distance_limit: Diff.color_distance_limit,
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
78
87
 
79
88
  if @screenshot_counter
80
- name = "#{format('%02i', @screenshot_counter)}_#{name}"
89
+ name = "#{format("%02i", @screenshot_counter)}_#{name}"
81
90
  @screenshot_counter += 1
82
91
  end
83
92
  name = full_name(name)
84
93
  file_name = "#{Screenshot.screenshot_area_abs}/#{name}.png"
85
94
 
86
95
  FileUtils.mkdir_p File.dirname(file_name)
87
- comparison = ImageCompare.new(file_name,
88
- dimensions: Screenshot.window_size, color_distance_limit: color_distance_limit,
89
- area_size_limit: area_size_limit, shift_distance_limit: shift_distance_limit,
90
- skip_area: skip_area)
96
+ comparison = ImageCompare.new(file_name, **driver_options)
91
97
  checkout_vcs(name, comparison)
92
- take_stable_screenshot(comparison, color_distance_limit: color_distance_limit,
93
- shift_distance_limit: shift_distance_limit,
94
- area_size_limit: area_size_limit,
95
- skip_area: skip_area)
96
- 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?
97
110
 
98
111
  (@test_screenshots ||= []) << [caller(1..1).first, name, comparison]
112
+
99
113
  true
100
114
  end
101
115
 
102
116
  def window_size_is_wrong?
103
117
  selenium? && Screenshot.window_size &&
104
-
105
- # FIXME(uwe): This happens with headless chrome. Why?!
106
- page.driver.browser.manage.window.size.width &&
107
- # EMXIF
108
-
109
118
  page.driver.browser.manage.window.size !=
110
119
  ::Selenium::WebDriver::Dimension.new(*Screenshot.window_size)
111
120
  end
@@ -113,21 +122,7 @@ def window_size_is_wrong?
113
122
  def assert_image_not_changed(caller, name, comparison)
114
123
  return unless comparison.different?
115
124
 
116
- # TODO(uwe): Remove check when we stop supporting Ruby 2.3 and older
117
- max_color_distance = if RUBY_VERSION >= '2.4'
118
- comparison.max_color_distance.ceil(1)
119
- else
120
- comparison.max_color_distance.ceil
121
- end
122
- # ODOT
123
-
124
- max_shift_distance = comparison.max_shift_distance
125
- "Screenshot does not match for '#{name}' (area: #{comparison.size}px #{comparison.dimensions}" \
126
- ", max_color_distance: #{max_color_distance}" \
127
- "#{", max_shift_distance: #{max_shift_distance}" if max_shift_distance})\n" \
128
- "#{comparison.new_file_name}\n#{comparison.annotated_old_file_name}\n" \
129
- "#{comparison.annotated_new_file_name}\n" \
130
- "at #{caller}"
125
+ "Screenshot does not match for '#{name}' #{comparison.error_message}\nat #{caller}"
131
126
  end
132
127
  end
133
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.1.0'
6
+ VERSION = "1.4.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"