capybara-screenshot-diff 1.3.1 → 1.4.0

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,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"