capybara-screenshot-diff 1.2.0 → 1.4.1

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:)
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 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)
@@ -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,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.0'
6
+ VERSION = "1.4.1"
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"