capybara-screenshot-diff 1.3.1 → 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:, stability_time_limit:, wait:)
25
- blurred_input = prepare_page_for_screenshot(timeout: wait)
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 stability_time_limit
32
-
33
30
  if comparison.quick_equal?
34
31
  clean_stabilization_images(comparison.new_file_name)
35
32
  break
@@ -37,10 +34,11 @@ 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
43
  if (Time.now - last_image_change_at) > stability_time_limit
46
44
  clean_stabilization_images(comparison.new_file_name)
@@ -52,39 +50,54 @@ def take_stable_screenshot(comparison, color_distance_limit:, shift_distance_lim
52
50
  end
53
51
  end
54
52
 
55
- previous_file_name = "#{comparison.new_file_name.chomp('.png')}" \
56
- "_x#{format('%02i', i)}_#{(Time.now - screenshot_started_at).round(1)}s" \
57
- "_#{stabilization_comparison.dimensions&.to_s&.gsub(', ', '_') || :initial}.png~"
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~"
58
56
  FileUtils.mv comparison.new_file_name, previous_file_name
59
57
 
60
- check_max_wait_time(comparison, screenshot_started_at,
61
- wait: wait, shift_distance_limit: shift_distance_limit)
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
62
71
  end
63
- ensure
64
- blurred_input&.click
65
72
  end
66
73
 
67
74
  private
68
75
 
69
- 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)
70
81
  return if !ON_MAC || !selenium? || !Capybara::Screenshot.window_size
71
82
 
72
- saved_image = ChunkyPNG::Image.from_file(file_name)
73
- width = Capybara::Screenshot.window_size[0]
74
- 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
75
86
 
76
- unless @_csd_retina_warned
77
- warn 'Halving retina screenshot. ' \
78
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
79
- @_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)
80
96
  end
81
- height = (width * saved_image.height) / saved_image.width
82
- resized_image = saved_image.resample_bilinear(width, height)
83
- resized_image.save(file_name)
84
97
  end
85
98
 
86
99
  def stabilization_images(base_file)
87
- Dir["#{base_file.chomp('.png')}_x*.png~"].sort
100
+ Dir["#{base_file.chomp(".png")}_x*.png~"].sort
88
101
  end
89
102
 
90
103
  def clean_stabilization_images(base_file)
@@ -104,7 +117,15 @@ def prepare_page_for_screenshot(timeout:)
104
117
  JS
105
118
  blurred_input = page.driver.send :unwrap_script_result, active_element
106
119
  end
107
- 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
108
129
  blurred_input
109
130
  end
110
131
 
@@ -112,32 +133,40 @@ def take_right_size_screenshot(comparison)
112
133
  save_screenshot(comparison.new_file_name)
113
134
 
114
135
  # TODO(uwe): Remove when chromedriver takes right size screenshots
115
- reduce_retina_image_size(comparison.new_file_name)
136
+ reduce_retina_image_size(comparison.new_file_name, comparison.driver)
116
137
  # ODOT
117
138
  end
118
139
 
119
- def check_max_wait_time(comparison, screenshot_started_at, wait:, shift_distance_limit:)
120
- shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
121
- max_wait_time = wait * shift_factor
140
+ def check_max_wait_time(comparison, screenshot_started_at, max_wait_time:)
122
141
  return if (Time.now - screenshot_started_at) < max_wait_time
123
142
 
143
+ annotate_stabilization_images(comparison)
124
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)
125
150
  previous_file = comparison.old_file_name
126
151
  stabilization_images(comparison.new_file_name).each do |file_name|
127
152
  if File.exist? previous_file
128
- stabilization_comparison =
129
- ImageCompare.new(file_name, previous_file,
130
- color_distance_limit: comparison.color_distance_limit,
131
- shift_distance_limit: comparison.shift_distance_limit,
132
- area_size_limit: comparison.area_size_limit, skip_area: comparison.skip_area)
133
- assert stabilization_comparison.different?
134
- FileUtils.mv stabilization_comparison.annotated_new_file_name, file_name
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
135
161
  FileUtils.rm stabilization_comparison.annotated_old_file_name
136
162
  end
137
163
  previous_file = file_name
138
164
  end
139
- fail("Could not get stable screenshot within #{max_wait_time}s\n" \
140
- "#{stabilization_images(comparison.new_file_name).join("\n")}")
165
+ end
166
+
167
+ def max_wait_time(shift_distance_limit, wait)
168
+ shift_factor = shift_distance_limit ? (shift_distance_limit * 2 + 1) ^ 2 : 1
169
+ wait * shift_factor
141
170
  end
142
171
 
143
172
  def assert_images_loaded(timeout:)
@@ -148,8 +177,11 @@ def assert_images_loaded(timeout:)
148
177
  pending_image = evaluate_script IMAGE_WAIT_SCRIPT
149
178
  break unless pending_image
150
179
 
151
- assert((Time.now - start) < timeout,
152
- "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
+
153
185
  sleep 0.1
154
186
  end
155
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,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.1'
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"