capybara-screenshot-diff 1.6.3 → 1.7.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.
@@ -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(