capybara-screenshot-diff 1.6.3 → 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -29,6 +29,10 @@ module Capybara
29
29
  reset
30
30
  end
31
31
 
32
+ def skip_area=(new_skip_area)
33
+ # noop
34
+ end
35
+
32
36
  # Resets the calculated data about the comparison with regard to the "new_image".
33
37
  # Data about the original image is kept.
34
38
  def reset
@@ -53,12 +57,6 @@ module Capybara
53
57
  [region, diff_mask]
54
58
  end
55
59
 
56
- def size(region)
57
- return 0 unless region
58
-
59
- (region[2] - region[0]) * (region[3] - region[1])
60
- end
61
-
62
60
  def adds_error_details_to(_log)
63
61
  end
64
62
 
@@ -68,8 +66,16 @@ module Capybara
68
66
  dimension(i) == dimensions || i.width < dimensions[0] || i.height < dimensions[1]
69
67
  end
70
68
 
71
- def crop(dimensions, i)
72
- i.crop(*dimensions)
69
+ def crop(region, i)
70
+ result = i.crop(*region.to_top_left_corner_coordinates)
71
+
72
+ # FIXME: Vips is caching operations, and if we ware going to read the same file, he will use cached version for this
73
+ # so after we cropped files and stored in the same file, the next load will recover old version instead of cropped
74
+ # Workaround to make vips works with cropped versions
75
+ Vips.cache_set_max(0)
76
+ Vips.vips_cache_set_max(1000)
77
+
78
+ result
73
79
  end
74
80
 
75
81
  def filter_image_with_median(image, median_filter_window_size)
@@ -77,7 +83,7 @@ module Capybara
77
83
  end
78
84
 
79
85
  def add_black_box(memo, region)
80
- memo.draw_rect([0, 0, 0, 0], *region, fill: true)
86
+ memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
81
87
  end
82
88
 
83
89
  def chunky_png_comparator
@@ -131,10 +137,10 @@ module Capybara
131
137
  result
132
138
  end
133
139
 
134
- def dimension_changed?(org_image, new_image)
135
- return false if dimension(org_image) == dimension(new_image)
140
+ def dimension_changed?(old_image, new_image)
141
+ return false if dimension(old_image) == dimension(new_image)
136
142
 
137
- change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
143
+ change_msg = [old_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
138
144
  warn "Image size has changed for #{@new_file_name}: #{change_msg}"
139
145
 
140
146
  true
@@ -144,16 +150,16 @@ module Capybara
144
150
  [image.width, image.height]
145
151
  end
146
152
 
147
- def draw_rectangles(images, (left, top, right, bottom), rgba)
153
+ def draw_rectangles(images, region, rgba)
148
154
  images.map do |image|
149
- image.draw_rect(rgba, left - 1, top - 1, right - left + 2, bottom - top + 2)
155
+ image.draw_rect(rgba, region.left - 1, region.top - 1, region.width + 2, region.height + 2)
150
156
  end
151
157
  end
152
158
 
153
159
  class VipsUtil
154
160
  def self.difference(old_image, new_image, color_distance: 0)
155
161
  diff_mask = difference_mask(color_distance, new_image, old_image)
156
- difference_region_by(diff_mask)
162
+ difference_region_by(diff_mask).to_edge_coordinates
157
163
  end
158
164
 
159
165
  def self.difference_area(old_image, new_image, color_distance: 0)
@@ -171,14 +177,17 @@ module Capybara
171
177
  end
172
178
 
173
179
  def self.difference_region_by(diff_mask)
174
- columns, rows = diff_mask.project
180
+ columns, rows = diff_mask.bandor.project
175
181
 
176
182
  left = columns.profile[1].min
177
183
  right = columns.width - columns.flip("horizontal").profile[1].min
184
+
178
185
  top = rows.profile[0].min
179
186
  bottom = rows.height - rows.flip("vertical").profile[0].min
180
187
 
181
- [left, top, right, bottom]
188
+ return nil if right < left || bottom < top
189
+
190
+ Region.from_edge_coordinates(left, top, right, bottom)
182
191
  end
183
192
  end
184
193
  end
@@ -12,9 +12,8 @@ module Capybara
12
12
 
13
13
  attr_reader :driver, :driver_options
14
14
 
15
- attr_reader :annotated_new_file_name, :annotated_old_file_name, :area_size_limit,
16
- :color_distance_limit, :new_file_name, :old_file_name, :shift_distance_limit,
17
- :skip_area
15
+ attr_reader :annotated_new_file_name, :annotated_old_file_name, :new_file_name, :old_file_name, :skip_area
16
+ attr_accessor :shift_distance_limit, :area_size_limit, :color_distance_limit
18
17
 
19
18
  def initialize(new_file_name, old_file_name = nil, options = {})
20
19
  options = old_file_name if old_file_name.is_a?(Hash)
@@ -40,21 +39,23 @@ module Capybara
40
39
  super(@driver)
41
40
  end
42
41
 
42
+ def skip_area=(new_skip_area)
43
+ @skip_area = new_skip_area
44
+ driver.skip_area = @skip_area
45
+ end
46
+
43
47
  # Compare the two image files and return `true` or `false` as quickly as possible.
44
- # Return falsish if the old file does not exist or the image dimensions do not match.
48
+ # Return falsely if the old file does not exist or the image dimensions do not match.
45
49
  def quick_equal?
46
50
  return false unless old_file_exists?
47
51
  return true if new_file_size == old_file_size
48
52
 
49
- # old_bytes, new_bytes = load_image_files(@old_file_name, @new_file_name)
50
- # return true if old_bytes == new_bytes
51
-
52
53
  images = driver.load_images(@old_file_name, @new_file_name)
53
54
  old_image, new_image = preprocess_images(images, driver)
54
55
 
55
56
  return false if driver.dimension_changed?(old_image, new_image)
56
57
 
57
- region, meta = driver.find_difference_region(
58
+ self.difference_region, meta = driver.find_difference_region(
58
59
  new_image,
59
60
  old_image,
60
61
  @color_distance_limit,
@@ -63,14 +64,9 @@ module Capybara
63
64
  fast_fail: true
64
65
  )
65
66
 
66
- self.difference_region = region
67
-
68
- return true if difference_region_empty?(new_image, region)
69
-
70
- return true if @area_size_limit && driver.size(region) <= @area_size_limit
71
-
72
- return true if @tolerance && @tolerance >= driver.difference_level(meta, old_image, region)
73
-
67
+ return true if difference_region_area_size.zero? || difference_region_empty?(new_image, difference_region)
68
+ return true if @area_size_limit && difference_region_area_size <= @area_size_limit
69
+ return true if @tolerance && @tolerance >= driver.difference_level(meta, old_image, difference_region)
74
70
  # TODO: Remove this or find similar solution for vips
75
71
  return true if @shift_distance_limit && driver.shift_distance_equal?
76
72
 
@@ -79,41 +75,38 @@ module Capybara
79
75
 
80
76
  # Compare the two images referenced by this object, and return `true` if they are different,
81
77
  # and `false` if they are the same.
82
- # Return `nil` if the old file does not exist or if the image dimensions do not match.
83
78
  def different?
84
- return nil unless old_file_exists?
79
+ return false unless old_file_exists?
85
80
 
86
81
  images = driver.load_images(@old_file_name, @new_file_name)
87
-
88
82
  old_image, new_image = preprocess_images(images, driver)
89
83
 
90
84
  if driver.dimension_changed?(old_image, new_image)
91
- save(new_image, old_image, @annotated_new_file_name, @annotated_old_file_name)
92
-
93
- self.difference_region = 0, 0, driver.width_for(old_image), driver.height_for(old_image)
85
+ self.difference_region = Region.from_edge_coordinates(
86
+ 0,
87
+ 0,
88
+ [driver.width_for(old_image), driver.width_for(new_image)].min,
89
+ [driver.height_for(old_image), driver.height_for(new_image)].min
90
+ )
94
91
 
95
- return true
92
+ return different(*images)
96
93
  end
97
94
 
98
- region, meta = driver.find_difference_region(
95
+ self.difference_region, meta = driver.find_difference_region(
99
96
  new_image,
100
97
  old_image,
101
98
  @color_distance_limit,
102
99
  @shift_distance_limit,
103
100
  @area_size_limit
104
101
  )
105
- self.difference_region = region
106
-
107
- return not_different if difference_region_empty?(old_image, region)
108
- return not_different if @area_size_limit && driver.size(region) <= @area_size_limit
109
- return not_different if @tolerance && @tolerance > driver.difference_level(meta, old_image, region)
110
102
 
103
+ return not_different if difference_region_area_size.zero? || difference_region_empty?(old_image, difference_region)
104
+ return not_different if @area_size_limit && difference_region_area_size <= @area_size_limit
105
+ return not_different if @tolerance && @tolerance > driver.difference_level(meta, old_image, difference_region)
111
106
  # TODO: Remove this or find similar solution for vips
112
107
  return not_different if @shift_distance_limit && !driver.shift_distance_different?
113
108
 
114
- annotate_and_save(images, region)
115
-
116
- true
109
+ different(*images)
117
110
  end
118
111
 
119
112
  def clean_tmp_files
@@ -123,17 +116,6 @@ module Capybara
123
116
  File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
124
117
  end
125
118
 
126
- DIFF_COLOR = [255, 0, 0, 255].freeze
127
- SKIP_COLOR = [255, 192, 0, 255].freeze
128
-
129
- def annotate_and_save(images, region = difference_region)
130
- annotated_images = driver.draw_rectangles(images, region, DIFF_COLOR)
131
- @skip_area.to_a.flatten.each_slice(4) do |region|
132
- annotated_images = driver.draw_rectangles(annotated_images, region, SKIP_COLOR)
133
- end
134
- save(*annotated_images, @annotated_old_file_name, @annotated_new_file_name)
135
- end
136
-
137
119
  def save(old_img, new_img, annotated_old_file_name, annotated_new_file_name)
138
120
  driver.save_image_to(old_img, annotated_old_file_name)
139
121
  driver.save_image_to(new_img, annotated_new_file_name)
@@ -148,25 +130,43 @@ module Capybara
148
130
  driver.reset
149
131
  end
150
132
 
133
+ NEW_LINE = "\n"
134
+
151
135
  def error_message
152
136
  result = {
153
- area_size: driver.size(difference_region),
154
- region: difference_region
137
+ area_size: difference_region_area_size,
138
+ region: difference_coordinates
155
139
  }
156
140
 
157
141
  driver.adds_error_details_to(result)
158
142
 
159
- ["(#{result.to_json})", new_file_name, annotated_old_file_name, annotated_new_file_name].join("\n")
143
+ [
144
+ "(#{result.to_json})",
145
+ new_file_name,
146
+ annotated_old_file_name,
147
+ annotated_new_file_name
148
+ ].join(NEW_LINE)
160
149
  end
161
150
 
162
- def difference_region
163
- return nil unless @left || @top || @right || @bottom
151
+ def difference_coordinates
152
+ difference_region&.to_edge_coordinates
153
+ end
154
+
155
+ def difference_region_area_size
156
+ return 0 unless difference_region
164
157
 
165
- [@left, @top, @right, @bottom]
158
+ difference_region.size
166
159
  end
167
160
 
168
161
  private
169
162
 
163
+ attr_accessor :difference_region
164
+
165
+ def different(old_image, new_image)
166
+ annotate_and_save([old_image, new_image], difference_region)
167
+ true
168
+ end
169
+
170
170
  def find_driver_class_for(driver)
171
171
  driver = AVAILABLE_DRIVERS.first if driver == :auto
172
172
 
@@ -183,23 +183,6 @@ module Capybara
183
183
  end
184
184
  end
185
185
 
186
- def old_file_size
187
- @old_file_size ||= old_file_exists? && File.size(@old_file_name)
188
- end
189
-
190
- def new_file_size
191
- File.size(@new_file_name)
192
- end
193
-
194
- def not_different
195
- clean_tmp_files
196
- false
197
- end
198
-
199
- def load_images(old_file_name, new_file_name, driver = self)
200
- [driver.from_file(old_file_name), driver.from_file(new_file_name)]
201
- end
202
-
203
186
  def preprocess_images(images, driver = self)
204
187
  old_img = preprocess_image(images.first, driver)
205
188
  new_img = preprocess_image(images.last, driver)
@@ -225,19 +208,49 @@ module Capybara
225
208
  result
226
209
  end
227
210
 
228
- def difference_region=(region)
229
- @left, @top, @right, @bottom = region
211
+ def old_file_size
212
+ @old_file_size ||= old_file_exists? && File.size(@old_file_name)
213
+ end
214
+
215
+ def new_file_size
216
+ File.size(@new_file_name)
217
+ end
218
+
219
+ def not_different
220
+ clean_tmp_files
221
+ false
230
222
  end
231
223
 
232
224
  def difference_region_empty?(new_image, region)
233
225
  region.nil? ||
234
226
  (
235
- region[1] == height_for(new_image) &&
236
- region[0] == width_for(new_image) &&
237
- region[2].zero? &&
238
- region[3].zero?
227
+ region.height == height_for(new_image) &&
228
+ region.width == width_for(new_image) &&
229
+ region.x.zero? &&
230
+ region.y.zero?
239
231
  )
240
232
  end
233
+
234
+ def annotate_and_save(images, region)
235
+ annotated_images = annotate_difference(images, region)
236
+ annotated_images = annotate_skip_areas(annotated_images, @skip_area) if @skip_area
237
+
238
+ save(*annotated_images, @annotated_old_file_name, @annotated_new_file_name)
239
+ end
240
+
241
+ DIFF_COLOR = [255, 0, 0, 255].freeze
242
+
243
+ def annotate_difference(images, region)
244
+ driver.draw_rectangles(images, region, DIFF_COLOR)
245
+ end
246
+
247
+ SKIP_COLOR = [255, 192, 0, 255].freeze
248
+
249
+ def annotate_skip_areas(annotated_images, skip_areas)
250
+ skip_areas.reduce(annotated_images) do |annotated_images, region|
251
+ driver.draw_rectangles(annotated_images, region, SKIP_COLOR)
252
+ end
253
+ end
241
254
  end
242
255
  end
243
256
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Region
4
+ attr_accessor :x, :y, :width, :height
5
+
6
+ def initialize(x, y, width, height)
7
+ @x, @y, @width, @height = x, y, width, height
8
+ end
9
+
10
+ def self.from_top_left_corner_coordinates(x, y, width, height)
11
+ return nil unless x && y && width && height
12
+ return nil if width < 0 || height < 0
13
+
14
+ Region.new(x, y, width, height)
15
+ end
16
+
17
+ def self.from_edge_coordinates(left, top, right, bottom)
18
+ return nil unless left && top && right && bottom
19
+ return nil if right < left || bottom < top
20
+
21
+ Region.new(left, top, right - left, bottom - top)
22
+ end
23
+
24
+ def to_edge_coordinates
25
+ [left, top, right, bottom]
26
+ end
27
+
28
+ def to_top_left_corner_coordinates
29
+ [x, y, width, height]
30
+ end
31
+
32
+ def top
33
+ y
34
+ end
35
+
36
+ def bottom
37
+ y + height
38
+ end
39
+
40
+ def left
41
+ x
42
+ end
43
+
44
+ def right
45
+ x + width
46
+ end
47
+
48
+ def size
49
+ return 0 if width < 0 || height < 0
50
+
51
+ result = width * height
52
+ result.zero? ? 1 : result
53
+ end
54
+
55
+ def to_a
56
+ [@x, @y, @width, @height]
57
+ end
58
+
59
+ def find_intersect_with(region)
60
+ return nil unless intersect?(region)
61
+
62
+ new_left = [x, region.x].max
63
+ new_top = [y, region.y].max
64
+
65
+ Region.new(new_left, new_top, [right, region.right].min - new_left, [bottom, region.bottom].min - new_top)
66
+ end
67
+
68
+ def intersect?(region)
69
+ left <= region.right && right >= region.left && top <= region.bottom && bottom >= region.top
70
+ end
71
+
72
+ def move_by(right_by, down_by)
73
+ Region.new(x + right_by, y + down_by, width, height)
74
+ end
75
+
76
+ def find_relative_intersect(region)
77
+ intersect = find_intersect_with(region)
78
+ return nil unless intersect
79
+
80
+ intersect.move_by(-x, -y)
81
+ end
82
+
83
+ def cover?(x, y)
84
+ left <= x && x <= right && top <= y && y <= bottom
85
+ end
86
+ end
@@ -8,28 +8,6 @@ module Capybara
8
8
  module Stabilization
9
9
  include Os
10
10
 
11
- IMAGE_WAIT_SCRIPT = <<-JS.strip_heredoc.freeze
12
- function pending_image() {
13
- var images = document.images;
14
- for (var i = 0; i < images.length; i++) {
15
- if (!images[i].complete) {
16
- return images[i].src;
17
- }
18
- }
19
- return false;
20
- }()
21
- JS
22
-
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
11
  def take_stable_screenshot(comparison, stability_time_limit:, wait:, crop:)
34
12
  previous_file_name = comparison.old_file_name
35
13
  screenshot_started_at = last_image_change_at = Time.now
@@ -41,6 +19,7 @@ module Capybara
41
19
  clean_stabilization_images(comparison.new_file_name)
42
20
  break
43
21
  end
22
+
44
23
  comparison.reset
45
24
 
46
25
  if previous_file_name
@@ -78,7 +57,7 @@ module Capybara
78
57
  end
79
58
 
80
59
  def notice_how_to_avoid_this
81
- unless @_csd_retina_warned
60
+ unless defined?(@_csd_retina_warned)
82
61
  warn "Halving retina screenshot. " \
83
62
  'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
84
63
  @_csd_retina_warned = true
@@ -90,7 +69,7 @@ module Capybara
90
69
  def build_snapshot_version_file_name(comparison, iteration, screenshot_started_at, stabilization_comparison)
91
70
  "#{comparison.new_file_name.chomp(".png")}" \
92
71
  "_x#{format("%02i", iteration)}_#{(Time.now - screenshot_started_at).round(1)}s" \
93
- "_#{stabilization_comparison.difference_region&.to_s&.gsub(", ", "_") || :initial}.png" \
72
+ "_#{stabilization_comparison.difference_coordinates&.to_s&.gsub(", ", "_") || :initial}.png" \
94
73
  "#{ImageCompare::TMP_FILE_SUFFIX}"
95
74
  end
96
75
 
@@ -123,18 +102,15 @@ module Capybara
123
102
 
124
103
  def prepare_page_for_screenshot(timeout:)
125
104
  assert_images_loaded(timeout: timeout)
105
+
126
106
  if Capybara::Screenshot.blur_active_element
127
- active_element = execute_script(<<-JS)
128
- ae = document.activeElement;
129
- if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
130
- ae.blur();
131
- return ae;
132
- }
133
- return null;
134
- JS
135
- blurred_input = page.driver.send :unwrap_script_result, active_element
107
+ blurred_input = blur_from_focused_element
136
108
  end
137
- execute_script(HIDE_CARET_SCRIPT) if Capybara::Screenshot.hide_caret
109
+
110
+ if Capybara::Screenshot.hide_caret
111
+ hide_caret
112
+ end
113
+
138
114
  blurred_input
139
115
  end
140
116
 
@@ -148,9 +124,9 @@ module Capybara
148
124
  # ODOT
149
125
 
150
126
  if crop
151
- full_img = driver.from_file(comparison.new_file_name)
152
- area_img = driver.crop([crop[0], crop[1], crop[2] - crop[0], crop[3] - crop[1]], full_img)
153
- driver.save_image_to(area_img, comparison.new_file_name)
127
+ image = driver.from_file(comparison.new_file_name)
128
+ cropped_image = driver.crop(crop, image)
129
+ driver.save_image_to(cropped_image, comparison.new_file_name)
154
130
  end
155
131
  end
156
132
 
@@ -191,7 +167,7 @@ module Capybara
191
167
 
192
168
  start = Time.now
193
169
  loop do
194
- pending_image = evaluate_script IMAGE_WAIT_SCRIPT
170
+ pending_image = pending_image_to_load
195
171
  break unless pending_image
196
172
 
197
173
  assert(