capybara-screenshot-diff 1.7.1 → 1.8.1

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +29 -0
  3. data/capybara-screenshot-diff.gemspec +6 -3
  4. data/gems.rb +8 -2
  5. data/lib/capybara/screenshot/diff/browser_helpers.rb +29 -28
  6. data/lib/capybara/screenshot/diff/cucumber.rb +11 -0
  7. data/lib/capybara/screenshot/diff/difference.rb +63 -0
  8. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +42 -0
  9. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +188 -260
  10. data/lib/capybara/screenshot/diff/drivers/utils.rb +18 -0
  11. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +54 -104
  12. data/lib/capybara/screenshot/diff/drivers.rb +16 -0
  13. data/lib/capybara/screenshot/diff/image_compare.rb +125 -154
  14. data/lib/capybara/screenshot/diff/os.rb +1 -1
  15. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +128 -0
  16. data/lib/capybara/screenshot/diff/screenshoter.rb +136 -0
  17. data/lib/capybara/screenshot/diff/stabilization.rb +0 -184
  18. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +106 -0
  19. data/lib/capybara/screenshot/diff/test_methods.rb +51 -90
  20. data/lib/capybara/screenshot/diff/vcs.rb +44 -22
  21. data/lib/capybara/screenshot/diff/version.rb +1 -1
  22. data/lib/capybara/screenshot/diff.rb +13 -17
  23. data/sig/capybara/screenshot/diff/diff.rbs +28 -0
  24. data/sig/capybara/screenshot/diff/difference.rbs +33 -0
  25. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +63 -0
  26. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +36 -0
  27. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +89 -0
  28. data/sig/capybara/screenshot/diff/drivers/utils.rbs +13 -0
  29. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +25 -0
  30. data/sig/capybara/screenshot/diff/image_compare.rbs +93 -0
  31. data/sig/capybara/screenshot/diff/os.rbs +11 -0
  32. data/sig/capybara/screenshot/diff/region.rbs +43 -0
  33. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +60 -0
  34. data/sig/capybara/screenshot/diff/screenshoter.rbs +48 -0
  35. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +29 -0
  36. data/sig/capybara/screenshot/diff/test_methods.rbs +39 -0
  37. data/sig/capybara/screenshot/diff/vcs.rbs +17 -0
  38. metadata +28 -25
  39. data/.gitattributes +0 -4
  40. data/.github/dependabot.yml +0 -8
  41. data/.github/workflows/lint.yml +0 -25
  42. data/.github/workflows/test.yml +0 -138
  43. data/.gitignore +0 -14
  44. data/.standard.yml +0 -12
  45. data/CONTRIBUTING.md +0 -24
  46. data/Dockerfile +0 -59
  47. data/README.md +0 -567
  48. data/bin/bundle +0 -114
  49. data/bin/console +0 -15
  50. data/bin/install-vips +0 -11
  51. data/bin/rake +0 -27
  52. data/bin/setup +0 -8
  53. data/bin/standardrb +0 -29
  54. data/gemfiles/rails60_gems.rb +0 -8
  55. data/gemfiles/rails61_gems.rb +0 -7
  56. data/gemfiles/rails70_gems.rb +0 -7
  57. data/tmp/.keep +0 -0
@@ -3,11 +3,11 @@
3
3
  begin
4
4
  require "vips"
5
5
  rescue LoadError => e
6
- warn 'Required ruby-vips gem is missing. Add `gem "ruby-vips"` to Gemfile' if e.message.include?("vips")
6
+ raise 'Required ruby-vips gem is missing. Add `gem "ruby-vips"` to Gemfile' if e.message.match?(/vips/i)
7
7
  raise
8
8
  end
9
9
 
10
- require_relative "./chunky_png_driver"
10
+ require "capybara/screenshot/diff/drivers/base_driver"
11
11
 
12
12
  module Capybara
13
13
  module Screenshot
@@ -15,67 +15,33 @@ module Capybara
15
15
  # Compare two images and determine if they are equal, different, or within some comparison
16
16
  # range considering color values and difference area size.
17
17
  module Drivers
18
- class VipsDriver
19
- attr_reader :new_file_name, :old_file_name, :options
18
+ class VipsDriver < BaseDriver
19
+ def find_difference_region(comparison)
20
+ new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options
20
21
 
21
- def initialize(new_file_name, old_file_name = nil, options = {})
22
- options = old_file_name if old_file_name.is_a?(Hash)
23
-
24
- @new_file_name = new_file_name
25
- @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}"
26
-
27
- @options = options || {}
28
-
29
- reset
30
- end
31
-
32
- def skip_area=(new_skip_area)
33
- # noop
34
- end
35
-
36
- # Resets the calculated data about the comparison with regard to the "new_image".
37
- # Data about the original image is kept.
38
- def reset
39
- end
40
-
41
- def shift_distance_equal?
42
- warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
43
- "please use median_filter_window_size and color_distance_limit options"
44
- chunky_png_comparator.quick_equal?
45
- end
46
-
47
- def shift_distance_different?
48
- warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
49
- "please use median_filter_window_size and color_distance_limit options"
50
- chunky_png_comparator.different?
51
- end
52
-
53
- def find_difference_region(new_image, old_image, color_distance_limit, _shift_distance_limit, _area_size_limit, fast_fail: false)
54
- diff_mask = VipsUtil.difference_mask(color_distance_limit, old_image, new_image)
22
+ diff_mask = VipsUtil.difference_mask(base_image, new_image, options[:color_distance_limit])
55
23
  region = VipsUtil.difference_region_by(diff_mask)
24
+ region = nil if region && same_as?(region, base_image)
56
25
 
57
- [region, diff_mask]
58
- end
59
-
60
- def adds_error_details_to(_log)
61
- end
26
+ result = Difference.new(region, {}, comparison)
62
27
 
63
- # old private
28
+ unless result.blank?
29
+ meta = {}
30
+ meta[:difference_level] = difference_level(diff_mask, base_image) if comparison.options[:tolerance]
31
+ result.meta = meta
32
+ end
64
33
 
65
- def inscribed?(dimensions, i)
66
- dimension(i) == dimensions || i.width < dimensions[0] || i.height < dimensions[1]
34
+ result
67
35
  end
68
36
 
69
37
  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
38
+ i.crop(*region.to_top_left_corner_coordinates)
39
+ rescue Vips::Error => e
40
+ warn(
41
+ "[capybara-screenshot-diff] Crop has been failed for " \
42
+ "{ region: #{region.to_top_left_corner_coordinates.inspect}, image: #{dimension(i).join("x")} }"
43
+ )
44
+ raise e
79
45
  end
80
46
 
81
47
  def filter_image_with_median(image, median_filter_window_size)
@@ -86,84 +52,66 @@ module Capybara
86
52
  memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
87
53
  end
88
54
 
89
- def chunky_png_comparator
90
- @chunky_png_comparator ||= ImageCompare.new(
91
- @new_file_name,
92
- @old_file_name,
93
- **@options.merge(driver: :chunky_png, tolerance: nil, median_filter_window_size: nil)
94
- )
95
- end
96
-
97
55
  def difference_level(diff_mask, old_img, _region = nil)
98
56
  VipsUtil.difference_area_size_by(diff_mask).to_f / image_area_size(old_img)
99
57
  end
100
58
 
101
- def image_area_size(old_img)
102
- width_for(old_img) * height_for(old_img)
103
- end
104
-
105
- def height_for(image)
106
- image.height
107
- end
108
-
109
- def width_for(image)
110
- image.width
111
- end
112
-
113
- PNG_EXTENSION = ".png"
59
+ MAX_FILENAME_LENGTH = 200
114
60
 
115
61
  # Vips could not work with the same file. Per each process we require to create new file
116
62
  def save_image_to(image, filename)
117
- ::Dir::Tmpname.create([filename, PNG_EXTENSION]) do |tmp_image_filename|
63
+ # Dir::Tmpname will happily produce tempfile names that are too long for most unix filesystems,
64
+ # which leads to "unix error: File name too long". Apply a limit to avoid this.
65
+ limited_filename = filename.to_s[-MAX_FILENAME_LENGTH..] || filename.to_s
66
+ ::Dir::Tmpname.create([limited_filename, PNG_EXTENSION]) do |tmp_image_filename|
118
67
  image.write_to_file(tmp_image_filename)
119
68
  FileUtils.mv(tmp_image_filename, filename)
120
69
  end
121
70
  end
122
71
 
123
72
  def resize_image_to(image, new_width, new_height)
124
- image.resize(1.* new_width / new_height)
73
+ image.resize(new_width.to_f / new_height)
125
74
  end
126
75
 
127
- def load_images(old_file_name, new_file_name, driver = self)
128
- [driver.from_file(old_file_name), driver.from_file(new_file_name)]
76
+ def load_images(old_file_name, new_file_name)
77
+ [from_file(old_file_name), from_file(new_file_name)]
129
78
  end
130
79
 
131
80
  def from_file(filename)
132
- result = ::Vips::Image.new_from_file(filename)
81
+ result = ::Vips::Image.new_from_file(filename.to_s)
133
82
 
134
- result = result.colourspace("srgb") if result.bands < 3
83
+ result = result.colourspace(:srgb) if result.bands < 3
135
84
  result = result.bandjoin(255) if result.bands == 3
136
85
 
137
86
  result
138
87
  end
139
88
 
140
- def dimension_changed?(old_image, new_image)
141
- return false if dimension(old_image) == dimension(new_image)
142
-
143
- change_msg = [old_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
144
- warn "Image size has changed for #{@new_file_name}: #{change_msg}"
145
-
146
- true
147
- end
148
-
149
89
  def dimension(image)
150
- [image.width, image.height]
90
+ [width_for(image), height_for(image)]
151
91
  end
152
92
 
153
- def draw_rectangles(images, region, rgba)
93
+ def draw_rectangles(images, region, rgba, offset: 0)
154
94
  images.map do |image|
155
- image.draw_rect(rgba, region.left - 1, region.top - 1, region.width + 2, region.height + 2)
95
+ image.draw_rect(rgba, region.left - offset, region.top - offset, region.width + (offset * 2), region.height + (offset * 2))
156
96
  end
157
97
  end
158
98
 
159
- class VipsUtil
160
- def self.difference(old_image, new_image, color_distance: 0)
161
- diff_mask = difference_mask(color_distance, new_image, old_image)
162
- difference_region_by(diff_mask).to_edge_coordinates
163
- end
99
+ def same_pixels?(comparison)
100
+ (comparison.new_image == comparison.base_image).min == 255
101
+ end
164
102
 
103
+ private
104
+
105
+ def same_as?(region, base_image)
106
+ region.x.zero? &&
107
+ region.y.zero? &&
108
+ region.height == height_for(base_image) &&
109
+ region.width == width_for(base_image)
110
+ end
111
+
112
+ class VipsUtil
165
113
  def self.difference_area(old_image, new_image, color_distance: 0)
166
- difference_mask = difference_mask(color_distance, new_image, old_image)
114
+ difference_mask = difference_mask(new_image, old_image, color_distance)
167
115
  difference_area_size_by(difference_mask)
168
116
  end
169
117
 
@@ -172,18 +120,20 @@ module Capybara
172
120
  diff_mask.hist_find.to_a[0][0].max
173
121
  end
174
122
 
175
- def self.difference_mask(color_distance, old_image, new_image)
176
- (new_image - old_image).abs > color_distance
123
+ def self.difference_mask(base_image, new_image, color_distance = nil)
124
+ result = (new_image - base_image).abs
125
+
126
+ color_distance ? result > color_distance : result
177
127
  end
178
128
 
179
129
  def self.difference_region_by(diff_mask)
180
130
  columns, rows = diff_mask.bandor.project
181
131
 
182
132
  left = columns.profile[1].min
183
- right = columns.width - columns.flip("horizontal").profile[1].min
133
+ right = columns.width - columns.flip(:horizontal).profile[1].min
184
134
 
185
135
  top = rows.profile[0].min
186
- bottom = rows.height - rows.flip("vertical").profile[0].min
136
+ bottom = rows.height - rows.flip(:vertical).profile[0].min
187
137
 
188
138
  return nil if right < left || bottom < top
189
139
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Screenshot
5
+ module Diff
6
+ module Drivers
7
+ def self.for(driver_options = {})
8
+ driver_option = driver_options.fetch(:driver, :chunky_png)
9
+ return driver_option unless driver_option.is_a?(Symbol)
10
+
11
+ Utils.find_driver_class_for(driver_option).new
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -5,211 +5,186 @@ module Capybara
5
5
  module Diff
6
6
  LOADED_DRIVERS = {}
7
7
 
8
- # Compare two images and determine if they are equal, different, or within some comparison
8
+ # Compare two image and determine if they are equal, different, or within some comparison
9
9
  # range considering color values and difference area size.
10
- class ImageCompare < SimpleDelegator
11
- TMP_FILE_SUFFIX = "~"
10
+ class ImageCompare
11
+ TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
12
12
 
13
13
  attr_reader :driver, :driver_options
14
14
 
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
15
+ attr_reader :annotated_image_path, :annotated_base_image_path,
16
+ :image_path, :base_image_path,
17
+ :new_file_name, :old_file_name
17
18
 
18
- def initialize(new_file_name, old_file_name = nil, options = {})
19
- options = old_file_name if old_file_name.is_a?(Hash)
19
+ def initialize(image_path, base_image_path, options = {})
20
+ @image_path = Pathname.new(image_path)
20
21
 
21
- @new_file_name = new_file_name
22
- @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}"
23
- @annotated_old_file_name = "#{new_file_name.chomp(".png")}.committed.png"
24
- @annotated_new_file_name = "#{new_file_name.chomp(".png")}.latest.png"
22
+ @new_file_name = @image_path.to_s
23
+ @annotated_image_path = @image_path.sub_ext(".diff.png")
25
24
 
26
- @driver_options = options
25
+ @base_image_path = Pathname.new(base_image_path)
27
26
 
28
- @color_distance_limit = options[:color_distance_limit] || 0
29
- @area_size_limit = options[:area_size_limit]
30
- @shift_distance_limit = options[:shift_distance_limit]
31
- @dimensions = options[:dimensions]
32
- @skip_area = options[:skip_area]
33
- @tolerance = options[:tolerance]
34
- @median_filter_window_size = options[:median_filter_window_size]
27
+ @old_file_name = @base_image_path.to_s
28
+ @annotated_base_image_path = @base_image_path.sub_ext(".diff.png")
35
29
 
36
- driver_klass = find_driver_class_for(@driver_options.fetch(:driver, :chunky_png))
37
- @driver = driver_klass.new(@new_file_name, @old_file_name, **@driver_options)
30
+ @driver_options = options.dup
38
31
 
39
- super(@driver)
40
- end
41
-
42
- def skip_area=(new_skip_area)
43
- @skip_area = new_skip_area
44
- driver.skip_area = @skip_area
32
+ @driver = Drivers.for(@driver_options)
45
33
  end
46
34
 
47
35
  # Compare the two image files and return `true` or `false` as quickly as possible.
48
36
  # Return falsely if the old file does not exist or the image dimensions do not match.
49
37
  def quick_equal?
50
- return false unless old_file_exists?
38
+ @error_message = nil
39
+ return false unless image_files_exist?
40
+ # TODO: Confirm this change. There are screenshots with the same size, but there is a big difference
51
41
  return true if new_file_size == old_file_size
52
42
 
53
- images = driver.load_images(@old_file_name, @new_file_name)
54
- old_image, new_image = preprocess_images(images, driver)
43
+ comparison = load_and_process_images
55
44
 
56
- return false if driver.dimension_changed?(old_image, new_image)
45
+ unless driver.same_dimension?(comparison)
46
+ @error_message = build_error_for_different_dimensions(comparison)
47
+ return false
48
+ end
57
49
 
58
- self.difference_region, meta = driver.find_difference_region(
59
- new_image,
60
- old_image,
61
- @color_distance_limit,
62
- @shift_distance_limit,
63
- @area_size_limit,
64
- fast_fail: true
65
- )
50
+ return true if driver.same_pixels?(comparison)
66
51
 
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)
70
- # TODO: Remove this or find similar solution for vips
71
- return true if @shift_distance_limit && driver.shift_distance_equal?
52
+ # Could not make any difference to be tolerable, so skip and return as not equal
53
+ return false if without_tolerable_options?
72
54
 
55
+ @difference = driver.find_difference_region(comparison)
56
+ return true unless @difference.different?
57
+
58
+ @error_message = @difference.inspect
73
59
  false
74
60
  end
75
61
 
76
- # Compare the two images referenced by this object, and return `true` if they are different,
62
+ # Compare the two image referenced by this object, and return `true` if they are different,
77
63
  # and `false` if they are the same.
78
64
  def different?
79
- return false unless old_file_exists?
65
+ @error_message = nil
80
66
 
81
- images = driver.load_images(@old_file_name, @new_file_name)
82
- old_image, new_image = preprocess_images(images, driver)
67
+ @error_message = _different?
83
68
 
84
- if driver.dimension_changed?(old_image, new_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
- )
69
+ clean_tmp_files unless @error_message
91
70
 
92
- return different(*images)
93
- end
71
+ !@error_message.nil?
72
+ end
73
+
74
+ def build_error_for_different_dimensions(comparison)
75
+ change_msg = [comparison.base_image, comparison.new_image]
76
+ .map { |i| driver.dimension(i).join("x") }
77
+ .join(" => ")
94
78
 
95
- self.difference_region, meta = driver.find_difference_region(
96
- new_image,
97
- old_image,
98
- @color_distance_limit,
99
- @shift_distance_limit,
100
- @area_size_limit
101
- )
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)
106
- # TODO: Remove this or find similar solution for vips
107
- return not_different if @shift_distance_limit && !driver.shift_distance_different?
108
-
109
- different(*images)
79
+ "Screenshot dimension has been changed for #{@new_file_name}: #{change_msg}"
110
80
  end
111
81
 
112
82
  def clean_tmp_files
113
- FileUtils.cp @old_file_name, @new_file_name if old_file_exists?
114
- File.delete(@old_file_name) if old_file_exists?
115
- File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
116
- File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
83
+ @annotated_base_image_path.unlink if @annotated_base_image_path.exist?
84
+ @annotated_image_path.unlink if @annotated_image_path.exist?
117
85
  end
118
86
 
119
- def save(old_img, new_img, annotated_old_file_name, annotated_new_file_name)
120
- driver.save_image_to(old_img, annotated_old_file_name)
121
- driver.save_image_to(new_img, annotated_new_file_name)
87
+ def save(image, image_path)
88
+ driver.save_image_to(image, image_path.to_s)
122
89
  end
123
90
 
124
- def old_file_exists?
125
- @old_file_name && File.exist?(@old_file_name)
91
+ def image_files_exist?
92
+ @base_image_path.exist? && @image_path.exist?
126
93
  end
127
94
 
128
- def reset
129
- self.difference_region = nil
130
- driver.reset
95
+ NEW_LINE = "\n"
96
+
97
+ attr_reader :error_message
98
+
99
+ private
100
+
101
+ def without_tolerable_options?
102
+ (@driver_options.keys & TOLERABLE_OPTIONS).empty?
131
103
  end
132
104
 
133
- NEW_LINE = "\n"
105
+ def _different?
106
+ raise "There is no original (base) screenshot version to compare, located: #{@base_image_path}" unless @base_image_path.exist?
107
+ raise "There is no new screenshot version to compare, located: #{@image_path}" unless @image_path.exist?
134
108
 
135
- def error_message
136
- result = {
137
- area_size: difference_region_area_size,
138
- region: difference_coordinates
139
- }
109
+ comparison = load_and_process_images
140
110
 
141
- driver.adds_error_details_to(result)
111
+ unless driver.same_dimension?(comparison)
112
+ return build_error_for_different_dimensions(comparison)
113
+ end
114
+
115
+ return not_different if driver.same_pixels?(comparison)
142
116
 
117
+ @difference = driver.find_difference_region(comparison)
118
+ return not_different unless @difference.different?
119
+
120
+ different(@difference)
121
+ end
122
+
123
+ def load_and_process_images
124
+ images = driver.load_images(old_file_name, new_file_name)
125
+ base_image, new_image = preprocess_images(images)
126
+ Comparison.new(new_image, base_image, @driver_options)
127
+ end
128
+
129
+ def build_error_message(difference)
143
130
  [
144
- "(#{result.to_json})",
131
+ "(#{difference.inspect})",
145
132
  new_file_name,
146
- annotated_old_file_name,
147
- annotated_new_file_name
133
+ annotated_base_image_path.to_path,
134
+ annotated_image_path.to_path
148
135
  ].join(NEW_LINE)
149
136
  end
150
137
 
151
- def difference_coordinates
152
- difference_region&.to_edge_coordinates
138
+ def skip_area
139
+ @driver_options[:skip_area]
153
140
  end
154
141
 
155
- def difference_region_area_size
156
- return 0 unless difference_region
157
-
158
- difference_region.size
142
+ def median_filter_window_size
143
+ @driver_options[:median_filter_window_size]
159
144
  end
160
145
 
161
- private
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
146
+ def dimensions
147
+ @driver_options[:dimensions]
168
148
  end
169
149
 
170
- def find_driver_class_for(driver)
171
- driver = AVAILABLE_DRIVERS.first if driver == :auto
172
-
173
- LOADED_DRIVERS[driver] ||=
174
- case driver
175
- when :chunky_png
176
- require "capybara/screenshot/diff/drivers/chunky_png_driver"
177
- Drivers::ChunkyPNGDriver
178
- when :vips
179
- require "capybara/screenshot/diff/drivers/vips_driver"
180
- Drivers::VipsDriver
181
- else
182
- fail "Wrong adapter #{driver.inspect}. Available adapters: #{AVAILABLE_DRIVERS.inspect}"
183
- end
150
+ def different(difference)
151
+ annotate_and_save_images(difference)
152
+ build_error_message(difference)
184
153
  end
185
154
 
186
- def preprocess_images(images, driver = self)
187
- old_img = preprocess_image(images.first, driver)
188
- new_img = preprocess_image(images.last, driver)
189
-
190
- [old_img, new_img]
155
+ def preprocess_images(images)
156
+ images.map { |image| preprocess_image(image) }
191
157
  end
192
158
 
193
- def preprocess_image(image, driver = self)
159
+ def preprocess_image(image)
194
160
  result = image
195
161
 
196
- if @dimensions && driver.inscribed?(@dimensions, result)
197
- result = driver.crop(@dimensions, result)
162
+ # FIXME: How can we access to this method from public interface? Is this not documented feature?
163
+ if dimensions && driver.inscribed?(dimensions, result)
164
+ result = driver.crop(dimensions, result)
198
165
  end
199
166
 
200
- if @median_filter_window_size
201
- result = driver.filter_image_with_median(image, @median_filter_window_size)
167
+ if skip_area
168
+ result = ignore_skipped_area(result)
202
169
  end
203
170
 
204
- if @skip_area
205
- result = @skip_area.reduce(result) { |image, region| driver.add_black_box(image, region) }
171
+ if median_filter_window_size
172
+ result = blur_image_by(image, median_filter_window_size)
206
173
  end
207
174
 
208
175
  result
209
176
  end
210
177
 
178
+ def blur_image_by(image, size)
179
+ driver.filter_image_with_median(image, size)
180
+ end
181
+
182
+ def ignore_skipped_area(image)
183
+ skip_area.reduce(image) { |memo, region| driver.add_black_box(memo, region) }
184
+ end
185
+
211
186
  def old_file_size
212
- @old_file_size ||= old_file_exists? && File.size(@old_file_name)
187
+ @old_file_size ||= image_files_exist? && File.size(@old_file_name)
213
188
  end
214
189
 
215
190
  def new_file_size
@@ -217,41 +192,37 @@ module Capybara
217
192
  end
218
193
 
219
194
  def not_different
220
- clean_tmp_files
221
- false
195
+ nil
222
196
  end
223
197
 
224
- def difference_region_empty?(new_image, region)
225
- region.nil? ||
226
- (
227
- region.height == height_for(new_image) &&
228
- region.width == width_for(new_image) &&
229
- region.x.zero? &&
230
- region.y.zero?
231
- )
198
+ def annotate_and_save_images(difference)
199
+ annotate_and_save_image(difference, difference.comparison.new_image, @annotated_image_path)
200
+ annotate_and_save_image(difference, difference.comparison.base_image, @annotated_base_image_path)
232
201
  end
233
202
 
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)
203
+ def annotate_and_save_image(difference, image, image_path)
204
+ image = annotate_difference(image, difference.region)
205
+ image = annotate_skip_areas(image, difference.skip_area) if difference.skip_area
206
+ save(image, image_path.to_path)
239
207
  end
240
208
 
241
209
  DIFF_COLOR = [255, 0, 0, 255].freeze
242
210
 
243
- def annotate_difference(images, region)
244
- driver.draw_rectangles(images, region, DIFF_COLOR)
211
+ def annotate_difference(image, region)
212
+ driver.draw_rectangles(Array[image], region, DIFF_COLOR, offset: 1).first
245
213
  end
246
214
 
247
215
  SKIP_COLOR = [255, 192, 0, 255].freeze
248
216
 
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)
217
+ def annotate_skip_areas(image, skip_areas)
218
+ skip_areas.reduce(image) do |memo, region|
219
+ driver.draw_rectangles(Array[memo], region, SKIP_COLOR).first
252
220
  end
253
221
  end
254
222
  end
223
+
224
+ class Comparison < Struct.new(:new_image, :base_image, :options)
225
+ end
255
226
  end
256
227
  end
257
228
  end
@@ -7,7 +7,7 @@ module Capybara
7
7
  ON_MAC = !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
8
8
  ON_LINUX = !!(RbConfig::CONFIG["host_os"] =~ /linux/)
9
9
 
10
- def os_name
10
+ def self.name
11
11
  return "windows" if ON_WINDOWS
12
12
  return "macos" if ON_MAC
13
13
  return "linux" if ON_LINUX