capybara-screenshot-diff 1.6.2 → 1.8.3

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 (58) 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 +102 -0
  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 +193 -252
  10. data/lib/capybara/screenshot/diff/drivers/utils.rb +25 -0
  11. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +65 -100
  12. data/lib/capybara/screenshot/diff/drivers.rb +16 -0
  13. data/lib/capybara/screenshot/diff/image_compare.rb +138 -154
  14. data/lib/capybara/screenshot/diff/os.rb +1 -1
  15. data/lib/capybara/screenshot/diff/region.rb +86 -0
  16. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +128 -0
  17. data/lib/capybara/screenshot/diff/screenshoter.rb +136 -0
  18. data/lib/capybara/screenshot/diff/stabilization.rb +0 -210
  19. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +106 -0
  20. data/lib/capybara/screenshot/diff/test_methods.rb +57 -63
  21. data/lib/capybara/screenshot/diff/vcs.rb +48 -21
  22. data/lib/capybara/screenshot/diff/version.rb +1 -1
  23. data/lib/capybara/screenshot/diff.rb +38 -35
  24. data/sig/capybara/screenshot/diff/diff.rbs +28 -0
  25. data/sig/capybara/screenshot/diff/difference.rbs +33 -0
  26. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +63 -0
  27. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +36 -0
  28. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +89 -0
  29. data/sig/capybara/screenshot/diff/drivers/utils.rbs +13 -0
  30. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +25 -0
  31. data/sig/capybara/screenshot/diff/image_compare.rbs +93 -0
  32. data/sig/capybara/screenshot/diff/os.rbs +11 -0
  33. data/sig/capybara/screenshot/diff/region.rbs +43 -0
  34. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +60 -0
  35. data/sig/capybara/screenshot/diff/screenshoter.rbs +48 -0
  36. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +29 -0
  37. data/sig/capybara/screenshot/diff/test_methods.rbs +39 -0
  38. data/sig/capybara/screenshot/diff/vcs.rbs +17 -0
  39. metadata +30 -25
  40. data/.gitattributes +0 -4
  41. data/.github/workflows/lint.yml +0 -25
  42. data/.github/workflows/test.yml +0 -120
  43. data/.gitignore +0 -12
  44. data/.standard.yml +0 -12
  45. data/CONTRIBUTING.md +0 -22
  46. data/Dockerfile +0 -60
  47. data/README.md +0 -555
  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/rails52.gemfile +0 -6
  55. data/gemfiles/rails60_gems.rb +0 -8
  56. data/gemfiles/rails61_gems.rb +0 -7
  57. data/gemfiles/rails70_gems.rb +0 -7
  58. 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,61 +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
- # Resets the calculated data about the comparison with regard to the "new_image".
33
- # Data about the original image is kept.
34
- def reset
35
- end
36
-
37
- def shift_distance_equal?
38
- warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
39
- "please use median_filter_window_size and color_distance_limit options"
40
- chunky_png_comparator.quick_equal?
41
- end
42
-
43
- def shift_distance_different?
44
- warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
45
- "please use median_filter_window_size and color_distance_limit options"
46
- chunky_png_comparator.different?
47
- end
48
-
49
- def find_difference_region(new_image, old_image, color_distance_limit, _shift_distance_limit, _area_size_limit, fast_fail: false)
50
- 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])
51
23
  region = VipsUtil.difference_region_by(diff_mask)
24
+ region = nil if region && same_as?(region, base_image)
52
25
 
53
- [region, diff_mask]
54
- end
55
-
56
- def size(region)
57
- return 0 unless region
58
-
59
- (region[2] - region[0]) * (region[3] - region[1])
60
- end
61
-
62
- def adds_error_details_to(_log)
63
- end
26
+ result = Difference.new(region, {}, comparison)
64
27
 
65
- # 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
66
33
 
67
- def inscribed?(dimensions, i)
68
- dimension(i) == dimensions || i.width < dimensions[0] || i.height < dimensions[1]
34
+ result
69
35
  end
70
36
 
71
- def crop(dimensions, i)
72
- i.crop(0, 0, *dimensions)
37
+ def crop(region, i)
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
73
45
  end
74
46
 
75
47
  def filter_image_with_median(image, median_filter_window_size)
@@ -77,81 +49,69 @@ module Capybara
77
49
  end
78
50
 
79
51
  def add_black_box(memo, region)
80
- memo.draw_rect([0, 0, 0, 0], *region, fill: true)
81
- end
82
-
83
- def chunky_png_comparator
84
- @chunky_png_comparator ||= ImageCompare.new(
85
- @new_file_name,
86
- @old_file_name,
87
- **@options.merge(driver: :chunky_png, tolerance: nil, median_filter_window_size: nil)
88
- )
52
+ memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
89
53
  end
90
54
 
91
55
  def difference_level(diff_mask, old_img, _region = nil)
92
56
  VipsUtil.difference_area_size_by(diff_mask).to_f / image_area_size(old_img)
93
57
  end
94
58
 
95
- def image_area_size(old_img)
96
- width_for(old_img) * height_for(old_img)
97
- end
98
-
99
- def height_for(image)
100
- image.height
101
- end
102
-
103
- def width_for(image)
104
- image.width
105
- end
59
+ MAX_FILENAME_LENGTH = 200
106
60
 
61
+ # Vips could not work with the same file. Per each process we require to create new file
107
62
  def save_image_to(image, filename)
108
- image.write_to_file(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|
67
+ image.write_to_file(tmp_image_filename)
68
+ FileUtils.mv(tmp_image_filename, filename)
69
+ end
109
70
  end
110
71
 
111
72
  def resize_image_to(image, new_width, new_height)
112
- image.resize(1.* new_width / new_height)
73
+ image.resize(new_width.to_f / new_height)
113
74
  end
114
75
 
115
- def load_images(old_file_name, new_file_name, driver = self)
116
- [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)]
117
78
  end
118
79
 
119
80
  def from_file(filename)
120
- result = ::Vips::Image.new_from_file(filename)
81
+ result = ::Vips::Image.new_from_file(filename.to_s)
121
82
 
122
- result = result.colourspace("srgb") if result.bands < 3
83
+ result = result.colourspace(:srgb) if result.bands < 3
123
84
  result = result.bandjoin(255) if result.bands == 3
124
85
 
125
86
  result
126
87
  end
127
88
 
128
- def dimension_changed?(org_image, new_image)
129
- return false if dimension(org_image) == dimension(new_image)
130
-
131
- change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
132
- warn "Image size has changed for #{@new_file_name}: #{change_msg}"
133
-
134
- true
135
- end
136
-
137
89
  def dimension(image)
138
- [image.width, image.height]
90
+ [width_for(image), height_for(image)]
139
91
  end
140
92
 
141
- def draw_rectangles(images, (left, top, right, bottom), rgba)
93
+ def draw_rectangles(images, region, rgba, offset: 0)
142
94
  images.map do |image|
143
- image.draw_rect(rgba, left - 1, top - 1, right - left + 2, bottom - top + 2)
95
+ image.draw_rect(rgba, region.left - offset, region.top - offset, region.width + (offset * 2), region.height + (offset * 2))
144
96
  end
145
97
  end
146
98
 
147
- class VipsUtil
148
- def self.difference(old_image, new_image, color_distance: 0)
149
- diff_mask = difference_mask(color_distance, new_image, old_image)
150
- difference_region_by(diff_mask)
151
- end
99
+ def same_pixels?(comparison)
100
+ (comparison.new_image == comparison.base_image).min == 255
101
+ end
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
152
111
 
112
+ class VipsUtil
153
113
  def self.difference_area(old_image, new_image, color_distance: 0)
154
- difference_mask = difference_mask(color_distance, new_image, old_image)
114
+ difference_mask = difference_mask(new_image, old_image, color_distance)
155
115
  difference_area_size_by(difference_mask)
156
116
  end
157
117
 
@@ -160,19 +120,24 @@ module Capybara
160
120
  diff_mask.hist_find.to_a[0][0].max
161
121
  end
162
122
 
163
- def self.difference_mask(color_distance, old_image, new_image)
164
- (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
165
127
  end
166
128
 
167
129
  def self.difference_region_by(diff_mask)
168
- columns, rows = diff_mask.project
130
+ columns, rows = diff_mask.bandor.project
169
131
 
170
132
  left = columns.profile[1].min
171
- right = columns.width - columns.flip("horizontal").profile[1].min
133
+ right = columns.width - columns.flip(:horizontal).profile[1].min
134
+
172
135
  top = rows.profile[0].min
173
- bottom = rows.height - rows.flip("vertical").profile[0].min
136
+ bottom = rows.height - rows.flip(:vertical).profile[0].min
137
+
138
+ return nil if right < left || bottom < top
174
139
 
175
- [left, top, right, bottom]
140
+ Region.from_edge_coordinates(left, top, right, bottom)
176
141
  end
177
142
  end
178
143
  end
@@ -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,186 +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, :area_size_limit,
16
- :color_distance_limit, :new_file_name, :old_file_name, :shift_distance_limit,
17
- :skip_area
15
+ attr_reader :annotated_image_path, :annotated_base_image_path,
16
+ :image_path, :base_image_path,
17
+ :new_file_name, :old_file_name
18
18
 
19
- def initialize(new_file_name, old_file_name = nil, options = {})
20
- 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)
21
21
 
22
- @new_file_name = new_file_name
23
- @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}"
24
- @annotated_old_file_name = "#{new_file_name.chomp(".png")}.committed.png"
25
- @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")
26
24
 
27
- @driver_options = options
25
+ @base_image_path = Pathname.new(base_image_path)
28
26
 
29
- @color_distance_limit = options[:color_distance_limit] || 0
30
- @area_size_limit = options[:area_size_limit]
31
- @shift_distance_limit = options[:shift_distance_limit]
32
- @dimensions = options[:dimensions]
33
- @skip_area = options[:skip_area]
34
- @tolerance = options[:tolerance]
35
- @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")
36
29
 
37
- driver_klass = find_driver_class_for(@driver_options.fetch(:driver, :chunky_png))
38
- @driver = driver_klass.new(@new_file_name, @old_file_name, **@driver_options)
30
+ @driver_options = options.dup
39
31
 
40
- super(@driver)
32
+ @driver = Drivers.for(@driver_options)
41
33
  end
42
34
 
43
35
  # 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.
36
+ # Return falsely if the old file does not exist or the image dimensions do not match.
45
37
  def quick_equal?
46
- 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
47
41
  return true if new_file_size == old_file_size
48
42
 
49
- # old_bytes, new_bytes = load_image_files(@old_file_name, @new_file_name)
50
- # return true if old_bytes == new_bytes
43
+ comparison = load_and_process_images
51
44
 
52
- images = driver.load_images(@old_file_name, @new_file_name)
53
- old_image, new_image = preprocess_images(images, driver)
54
-
55
- return false if driver.dimension_changed?(old_image, new_image)
56
-
57
- region, meta = driver.find_difference_region(
58
- new_image,
59
- old_image,
60
- @color_distance_limit,
61
- @shift_distance_limit,
62
- @area_size_limit,
63
- fast_fail: true
64
- )
65
-
66
- self.difference_region = region
67
-
68
- return true if difference_region_empty?(new_image, region)
45
+ unless driver.same_dimension?(comparison)
46
+ @error_message = build_error_for_different_dimensions(comparison)
47
+ return false
48
+ end
69
49
 
70
- return true if @area_size_limit && driver.size(region) <= @area_size_limit
50
+ return true if driver.same_pixels?(comparison)
71
51
 
72
- return true if @tolerance && @tolerance >= driver.difference_level(meta, old_image, region)
52
+ # Could not make any difference to be tolerable, so skip and return as not equal
53
+ return false if without_tolerable_options?
73
54
 
74
- # TODO: Remove this or find similar solution for vips
75
- return true if @shift_distance_limit && driver.shift_distance_equal?
55
+ @difference = driver.find_difference_region(comparison)
56
+ return true unless @difference.different?
76
57
 
58
+ @error_message = @difference.inspect
77
59
  false
78
60
  end
79
61
 
80
- # 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,
81
63
  # 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
64
  def different?
84
- return nil unless old_file_exists?
65
+ @error_message = nil
85
66
 
86
- images = driver.load_images(@old_file_name, @new_file_name)
67
+ @error_message = _different?
87
68
 
88
- old_image, new_image = preprocess_images(images, driver)
69
+ clean_tmp_files unless @error_message
89
70
 
90
- if driver.dimension_changed?(old_image, new_image)
91
- save(new_image, old_image, @annotated_new_file_name, @annotated_old_file_name)
71
+ !@error_message.nil?
72
+ end
92
73
 
93
- self.difference_region = 0, 0, driver.width_for(old_image), driver.height_for(old_image)
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
- return true
96
- end
79
+ "Screenshot dimension has been changed for #{@new_file_name}: #{change_msg}"
80
+ end
97
81
 
98
- region, meta = driver.find_difference_region(
99
- new_image,
100
- old_image,
101
- @color_distance_limit,
102
- @shift_distance_limit,
103
- @area_size_limit
104
- )
105
- self.difference_region = region
82
+ def clean_tmp_files
83
+ @annotated_base_image_path.unlink if @annotated_base_image_path.exist?
84
+ @annotated_image_path.unlink if @annotated_image_path.exist?
85
+ end
106
86
 
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)
87
+ def save(image, image_path)
88
+ driver.save_image_to(image, image_path.to_s)
89
+ end
110
90
 
111
- # TODO: Remove this or find similar solution for vips
112
- return not_different if @shift_distance_limit && !driver.shift_distance_different?
91
+ def image_files_exist?
92
+ @base_image_path.exist? && @image_path.exist?
93
+ end
113
94
 
114
- annotate_and_save(images, region)
95
+ NEW_LINE = "\n"
115
96
 
116
- true
117
- end
97
+ attr_reader :error_message
118
98
 
119
- def clean_tmp_files
120
- FileUtils.cp @old_file_name, @new_file_name if old_file_exists?
121
- File.delete(@old_file_name) if old_file_exists?
122
- File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
123
- File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
99
+ private
100
+
101
+ def without_tolerable_options?
102
+ (@driver_options.keys & TOLERABLE_OPTIONS).empty?
124
103
  end
125
104
 
126
- DIFF_COLOR = [255, 0, 0, 255].freeze
127
- SKIP_COLOR = [255, 192, 0, 255].freeze
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?
108
+
109
+ comparison = load_and_process_images
128
110
 
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)
111
+ unless driver.same_dimension?(comparison)
112
+ return build_error_for_different_dimensions(comparison)
133
113
  end
134
- save(*annotated_images, @annotated_old_file_name, @annotated_new_file_name)
135
- end
136
114
 
137
- def save(old_img, new_img, annotated_old_file_name, annotated_new_file_name)
138
- driver.save_image_to(old_img, annotated_old_file_name)
139
- driver.save_image_to(new_img, annotated_new_file_name)
115
+ return not_different if driver.same_pixels?(comparison)
116
+
117
+ @difference = driver.find_difference_region(comparison)
118
+ return not_different unless @difference.different?
119
+
120
+ different(@difference)
140
121
  end
141
122
 
142
- def old_file_exists?
143
- @old_file_name && File.exist?(@old_file_name)
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)
144
127
  end
145
128
 
146
- def reset
147
- self.difference_region = nil
148
- driver.reset
129
+ def build_error_message(difference)
130
+ [
131
+ "(#{difference.inspect})",
132
+ new_file_name,
133
+ annotated_base_image_path.to_path,
134
+ annotated_image_path.to_path
135
+ ].join(NEW_LINE)
149
136
  end
150
137
 
151
- def error_message
152
- result = {
153
- area_size: driver.size(difference_region),
154
- region: difference_region
155
- }
138
+ def skip_area
139
+ @driver_options[:skip_area]
140
+ end
156
141
 
157
- driver.adds_error_details_to(result)
142
+ def median_filter_window_size
143
+ @driver_options[:median_filter_window_size]
144
+ end
158
145
 
159
- ["(#{result.to_json})", new_file_name, annotated_old_file_name, annotated_new_file_name].join("\n")
146
+ def dimensions
147
+ @driver_options[:dimensions]
160
148
  end
161
149
 
162
- def difference_region
163
- return nil unless @left || @top || @right || @bottom
150
+ def different(difference)
151
+ annotate_and_save_images(difference)
152
+ build_error_message(difference)
153
+ end
164
154
 
165
- [@left, @top, @right, @bottom]
155
+ def preprocess_images(images)
156
+ images.map { |image| preprocess_image(image) }
166
157
  end
167
158
 
168
- private
159
+ def preprocess_image(image)
160
+ result = image
161
+
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)
165
+ end
169
166
 
170
- def find_driver_class_for(driver)
171
- driver = AVAILABLE_DRIVERS.first if driver == :auto
167
+ if skip_area
168
+ result = ignore_skipped_area(result)
169
+ end
172
170
 
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
171
+ if median_filter_window_size
172
+ result = blur_image_by(image, median_filter_window_size)
173
+ end
174
+
175
+ result
176
+ end
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
184
  end
185
185
 
186
186
  def old_file_size
187
- @old_file_size ||= old_file_exists? && File.size(@old_file_name)
187
+ @old_file_size ||= image_files_exist? && File.size(@old_file_name)
188
188
  end
189
189
 
190
190
  def new_file_size
@@ -192,52 +192,36 @@ module Capybara
192
192
  end
193
193
 
194
194
  def not_different
195
- clean_tmp_files
196
- false
195
+ nil
197
196
  end
198
197
 
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)]
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)
201
201
  end
202
202
 
203
- def preprocess_images(images, driver = self)
204
- old_img = preprocess_image(images.first, driver)
205
- new_img = preprocess_image(images.last, driver)
206
-
207
- [old_img, new_img]
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)
208
207
  end
209
208
 
210
- def preprocess_image(image, driver = self)
211
- result = image
209
+ DIFF_COLOR = [255, 0, 0, 255].freeze
212
210
 
213
- if @dimensions && driver.inscribed?(@dimensions, result)
214
- result = driver.crop(@dimensions, result)
215
- end
211
+ def annotate_difference(image, region)
212
+ driver.draw_rectangles(Array[image], region, DIFF_COLOR, offset: 1).first
213
+ end
216
214
 
217
- if @median_filter_window_size
218
- result = driver.filter_image_with_median(image, @median_filter_window_size)
219
- end
215
+ SKIP_COLOR = [255, 192, 0, 255].freeze
220
216
 
221
- if @skip_area
222
- result = @skip_area.reduce(result) { |image, region| driver.add_black_box(image, region) }
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
223
220
  end
224
-
225
- result
226
- end
227
-
228
- def difference_region=(region)
229
- @left, @top, @right, @bottom = region
230
221
  end
222
+ end
231
223
 
232
- def difference_region_empty?(new_image, region)
233
- region.nil? ||
234
- (
235
- region[1] == height_for(new_image) &&
236
- region[0] == width_for(new_image) &&
237
- region[2].zero? &&
238
- region[3].zero?
239
- )
240
- end
224
+ class Comparison < Struct.new(:new_image, :base_image, :options)
241
225
  end
242
226
  end
243
227
  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