capybara-screenshot-diff 1.6.2 → 1.8.3

Sign up to get free protection for your applications and to get access to all the features.
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