dotdiff 2.0.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +17 -0
- data/Gemfile +2 -0
- data/README.md +8 -4
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/dotdiff.gemspec +26 -17
- data/lib/dotdiff.rb +39 -7
- data/lib/dotdiff/command_wrapper.rb +8 -19
- data/lib/dotdiff/comparer.rb +17 -34
- data/lib/dotdiff/comparible/base.rb +47 -0
- data/lib/dotdiff/comparible/element_comparer.rb +29 -0
- data/lib/dotdiff/comparible/page_comparer.rb +22 -0
- data/lib/dotdiff/element_handler.rb +15 -12
- data/lib/dotdiff/element_meta.rb +11 -5
- data/lib/dotdiff/image/container.rb +41 -0
- data/lib/dotdiff/image/cropper.rb +4 -2
- data/lib/dotdiff/rspec_matcher.rb +2 -0
- data/lib/dotdiff/snapshot.rb +15 -11
- data/lib/dotdiff/threshold_calculator.rb +49 -0
- data/lib/dotdiff/version.rb +3 -1
- metadata +43 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dc1ca017c297dd63435e5282ad168d6e25ab0f9a3c8e2ac0caea8e2d2fa0df4e
|
4
|
+
data.tar.gz: bbecf64885b04e5ed2cd123d6b930e3ceaa15c82a03e0ee67937bb49af385537
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df25e2add82e31537d9d6cab28f46c96cd251106a331a503709c9aa33e229ae64837c809a03c66e4142def23e3eedf05f49106101ba343bac48b61faeff16740
|
7
|
+
data.tar.gz: 4ba8aafa62200080f35294ade8a7d1198f880de72385cc9196eff04654e72755daf3771084136639aa8a01f5c6844a78274b749cacd6a82570a01a1d8833fbe5
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
# The behavior of RuboCop can be controlled via the .rubocop.yml
|
4
|
+
# configuration file. It makes it possible to enable/disable
|
5
|
+
# certain cops (checks) and to alter their behavior if they accept
|
6
|
+
# any parameters. The file can be placed either in your home
|
7
|
+
# directory or in some project directory.
|
8
|
+
#
|
9
|
+
# RuboCop will start looking for the configuration file in the directory
|
10
|
+
# where the inspected file is and continue its way up to the root directory.
|
11
|
+
#
|
12
|
+
# See https://github.com/rubocop-hq/rubocop/blob/master/manual/configuration.md
|
13
|
+
AllCops:
|
14
|
+
NewCops: enable
|
15
|
+
TargetRubyVersion: 2.4
|
16
|
+
SuggestExtensions: false
|
17
|
+
|
18
|
+
Style/Documentation:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Metrics/BlockLength:
|
22
|
+
ExcludedMethods:
|
23
|
+
- describe
|
24
|
+
- context
|
25
|
+
Exclude:
|
26
|
+
- 'spec/unit/snapshot_spec.rb'
|
27
|
+
|
28
|
+
Layout/LineLength:
|
29
|
+
Max: 120
|
30
|
+
IgnoredPatterns:
|
31
|
+
- !ruby/regexp /\A +(it|describe|context|shared_examples|include_examples|it_behaves_like) ["']/
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2021-03-13 18:24:12 +0000 using RuboCop version 0.84.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 2
|
10
|
+
# Configuration parameters: IgnoredMethods.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Max: 17
|
13
|
+
|
14
|
+
# Offense count: 1
|
15
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
16
|
+
Metrics/MethodLength:
|
17
|
+
Max: 14
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -12,8 +12,11 @@ different display suchas username or user specific details, but only for full pa
|
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
15
|
+
|
15
16
|
Add this line to your application's Gemfile:
|
16
17
|
|
18
|
+
This is the same for JRuby platform as well (both MRI and JRuby versions of gem is uploaded)
|
19
|
+
|
17
20
|
```ruby
|
18
21
|
gem 'dotdiff'
|
19
22
|
```
|
@@ -48,12 +51,12 @@ In an initializer you can configure certain options example shown below within D
|
|
48
51
|
```ruby
|
49
52
|
DotDiff.configure do |config|
|
50
53
|
config.image_magick_diff_bin = `which compare`
|
51
|
-
config.pixel_threshold =
|
54
|
+
config.pixel_threshold = { type: 'percent', value: 0.04 }
|
52
55
|
config.image_magick_options = '-metric AE'
|
53
56
|
config.image_store_path = File.join('/home', 'user', 'images')
|
54
57
|
config.xpath_elements_to_hide = ["id('main')"]
|
58
|
+
config.hide_elements_on_non_full_screen_screenshot = true
|
55
59
|
config.failure_image_path = File.join('/home', 'user', 'failure_comparisions')
|
56
|
-
config.max_wait_time = 2
|
57
60
|
end
|
58
61
|
```
|
59
62
|
|
@@ -80,10 +83,11 @@ The only difference for the element specific is passing a specific element in th
|
|
80
83
|
| image_magick_diff_bin | Location of the image magick compare binary file | `which compare` | N/A | Yes |
|
81
84
|
| image_store_path | The root path to store the base images | File.join('/home', 'user','images') | nil | Yes |
|
82
85
|
| xpath_elements_to_hide | When taking full page screenshots it will hide elements specified that might change each time and re-shows them after the screenshot. It doesn't use this option for taking screenshots of specific elements. | ["id('main')", "//div[contains(@class, 'formy'])[1]"] | [] | No |
|
86
|
+
| hide_elements_on_non_full_screen_screenshot | When taking non full page screenshots whether to also hide elements using `xpath_elements_to_hide` or not hide anything at all | true | false | No |
|
83
87
|
| failure_image_path | When a comparison occurs and the perceptual_diff binary returns a failure with the message. It will dump the new image taken for comparison to this directory. If not supplied it will not move the images from the temporary location that it is generated at. | File.join('/home', 'user','failures') | nil | No |
|
84
|
-
|
|
85
|
-
| pixel_threshold | This validates the output from compare is within your specified threshold | 120 | 100 | No |
|
88
|
+
| pixel_threshold | This validates the output from compare is within your specified threshold_config which supports pixel or percent value | { type: 'percent', value: 0.03 } | { type: 'pixel', value: 100 } | No |
|
86
89
|
| image_magick_options | This allows you to pass some custom options to image magick | '-fuzz 10% -metric RSME' | '-fuzz 5% -metric AE' | No |
|
90
|
+
|
87
91
|
## Contributing
|
88
92
|
|
89
93
|
Bug reports and pull requests are welcome on GitHub at https://github.com/jnormington/dotdiff.
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'dotdiff'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "dotdiff"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start
|
data/dotdiff.gemspec
CHANGED
@@ -1,29 +1,38 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'dotdiff/version'
|
5
6
|
|
7
|
+
is_java = RUBY_PLATFORM == 'java'
|
8
|
+
|
6
9
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
10
|
+
spec.name = 'dotdiff'
|
8
11
|
spec.version = DotDiff::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
12
|
+
spec.authors = ['Jon Normington']
|
13
|
+
spec.email = ['jnormington@users.noreply.github.com']
|
14
|
+
spec.platform = 'java' if is_java
|
11
15
|
|
12
|
-
spec.summary =
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
spec.
|
16
|
+
spec.summary = 'Image regression wrapper for Capybara and RSpec using image'\
|
17
|
+
'magick supporting both MRI and JRuby versions'
|
18
|
+
spec.description = [spec.summary, 'which supports snap shoting both full page and'\
|
19
|
+
"specific elements on a page where text checks isn't enough"].join(' ')
|
20
|
+
spec.homepage = 'https://github.com/jnormington/dotdiff'
|
21
|
+
spec.license = 'MIT'
|
17
22
|
|
18
23
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
-
spec.bindir =
|
24
|
+
spec.bindir = 'exe'
|
20
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
-
spec.require_paths = [
|
26
|
+
spec.require_paths = ['lib']
|
22
27
|
|
23
|
-
|
28
|
+
if is_java
|
29
|
+
spec.add_runtime_dependency 'rmagick4j', '>= 0.4.0'
|
30
|
+
else
|
31
|
+
spec.add_runtime_dependency 'rmagick', '>= 2.15'
|
32
|
+
end
|
24
33
|
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
34
|
+
spec.add_development_dependency 'bundler', '>= 2'
|
35
|
+
spec.add_development_dependency 'capybara', '>= 2.6'
|
36
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
37
|
+
spec.add_development_dependency 'rspec', '>= 3.0'
|
29
38
|
end
|
data/lib/dotdiff.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dotdiff/version'
|
2
4
|
|
3
5
|
require 'shellwords'
|
@@ -9,16 +11,28 @@ require 'dotdiff/element_handler'
|
|
9
11
|
|
10
12
|
require 'dotdiff/element_meta'
|
11
13
|
require 'dotdiff/image/cropper'
|
14
|
+
require 'dotdiff/image/container'
|
12
15
|
require 'dotdiff/snapshot'
|
16
|
+
|
17
|
+
require 'dotdiff/threshold_calculator'
|
18
|
+
|
19
|
+
require 'dotdiff/comparible/base'
|
20
|
+
require 'dotdiff/comparible/page_comparer'
|
21
|
+
require 'dotdiff/comparible/element_comparer'
|
22
|
+
|
13
23
|
require 'dotdiff/comparer'
|
14
24
|
|
15
25
|
module DotDiff
|
26
|
+
class UnknownTypeError < StandardError; end
|
27
|
+
class InvalidValueError < StandardError; end
|
28
|
+
|
29
|
+
SUPPORTED_THRESHOLD_TYPES = %w[pixel percent].freeze
|
30
|
+
|
16
31
|
class << self
|
17
|
-
attr_accessor :
|
18
|
-
:image_store_path, :overwrite_on_resave, :xpath_elements_to_hide,
|
19
|
-
:max_wait_time
|
32
|
+
attr_accessor :failure_image_path, :image_store_path, :overwrite_on_resave
|
20
33
|
|
21
|
-
attr_writer :image_magick_options, :
|
34
|
+
attr_writer :image_magick_options, :image_magick_diff_bin,
|
35
|
+
:resave_base_image, :xpath_elements_to_hide, :hide_elements_on_non_full_screen_screenshot
|
22
36
|
|
23
37
|
def configure
|
24
38
|
yield self
|
@@ -32,6 +46,10 @@ module DotDiff
|
|
32
46
|
@xpath_elements_to_hide ||= []
|
33
47
|
end
|
34
48
|
|
49
|
+
def hide_elements_on_non_full_screen_screenshot
|
50
|
+
@hide_elements_on_non_full_screen_screenshot ||= false
|
51
|
+
end
|
52
|
+
|
35
53
|
def image_magick_options
|
36
54
|
@image_magick_options ||= '-fuzz 5% -metric AE'
|
37
55
|
end
|
@@ -41,11 +59,25 @@ module DotDiff
|
|
41
59
|
end
|
42
60
|
|
43
61
|
def pixel_threshold
|
44
|
-
@pixel_threshold ||= 100
|
62
|
+
@pixel_threshold ||= { type: 'pixel', value: 100 }
|
45
63
|
end
|
46
64
|
|
47
|
-
def
|
48
|
-
|
65
|
+
def pixel_threshold=(config)
|
66
|
+
unless config.class == Hash
|
67
|
+
Kernel.warn '[Dotdiff deprecation] Pass a hash options instead of integer to support pixel/percentage threshold'
|
68
|
+
@pixel_threshold = config
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
unless SUPPORTED_THRESHOLD_TYPES.include?(config.fetch(:type))
|
73
|
+
raise UnknownTypeError, "Unknown threshold type supports only: #{SUPPORTED_THRESHOLD_TYPES.join(',')}"
|
74
|
+
end
|
75
|
+
|
76
|
+
if config.fetch(:type) == 'percent' && config.fetch(:value) > 1
|
77
|
+
raise InvalidValueError, 'Percent value should be a float between 0 and 1'
|
78
|
+
end
|
79
|
+
|
80
|
+
@pixel_threshold = config
|
49
81
|
end
|
50
82
|
end
|
51
83
|
end
|
@@ -1,26 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'shellwords'
|
2
4
|
|
3
5
|
module DotDiff
|
4
6
|
class CommandWrapper
|
5
|
-
attr_reader :message
|
7
|
+
attr_reader :message, :pixels
|
6
8
|
|
7
9
|
def run(base_image, new_image, diff_image_path)
|
8
10
|
output = run_command(base_image, new_image, diff_image_path)
|
9
|
-
|
10
|
-
@ran_checks = true
|
11
|
+
@message = output
|
11
12
|
|
12
13
|
begin
|
13
|
-
pixels = Float(output)
|
14
|
-
|
15
|
-
|
16
|
-
@failed = false
|
17
|
-
else
|
18
|
-
@failed = true
|
19
|
-
@message = "Images are #{pixels} pixels different"
|
20
|
-
end
|
21
|
-
rescue ArgumentError => e
|
14
|
+
@pixels = Float(output)
|
15
|
+
@failed = false
|
16
|
+
rescue ArgumentError
|
22
17
|
@failed = true
|
23
|
-
@message = output
|
24
18
|
end
|
25
19
|
end
|
26
20
|
|
@@ -29,16 +23,11 @@ module DotDiff
|
|
29
23
|
end
|
30
24
|
|
31
25
|
def failed?
|
32
|
-
@
|
33
|
-
end
|
34
|
-
|
35
|
-
def ran_checks
|
36
|
-
@ran_checks
|
26
|
+
@failed
|
37
27
|
end
|
38
28
|
|
39
29
|
private
|
40
30
|
|
41
|
-
# For the tests
|
42
31
|
def run_command(base_image, new_image, diff_image_path)
|
43
32
|
`#{command(base_image, new_image, diff_image_path)}`.strip
|
44
33
|
end
|
data/lib/dotdiff/comparer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DotDiff
|
2
4
|
class Comparer
|
3
5
|
attr_reader :element, :page, :snapshot
|
@@ -9,50 +11,31 @@ module DotDiff
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def result
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
compare_element
|
16
|
-
else
|
14
|
+
comparer = build_comparer
|
15
|
+
|
16
|
+
if comparer.nil?
|
17
17
|
raise ArgumentError, "Unknown element class received: #{element.class.name}"
|
18
18
|
end
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
19
|
|
23
|
-
|
24
|
-
|
25
|
-
snapshot.crop_and_resave(element_meta)
|
20
|
+
passed, msg = comparer.run
|
21
|
+
write_failure_imgs(comparer.new_image_path) if !passed && DotDiff.failure_image_path
|
26
22
|
|
27
|
-
|
28
|
-
snapshot.resave_cropped_file
|
29
|
-
[true, snapshot.basefile]
|
30
|
-
else
|
31
|
-
compare(snapshot.cropped_file)
|
32
|
-
end
|
23
|
+
[passed, msg]
|
33
24
|
end
|
34
25
|
|
35
|
-
|
36
|
-
snapshot.capture_from_browser
|
26
|
+
private
|
37
27
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
else
|
42
|
-
compare(snapshot.fullscreen_file)
|
43
|
-
end
|
28
|
+
def write_failure_imgs(new_image_path)
|
29
|
+
FileUtils.mkdir_p(snapshot.failure_path)
|
30
|
+
FileUtils.mv(new_image_path, snapshot.new_file, force: true)
|
44
31
|
end
|
45
32
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
FileUtils.mkdir_p(snapshot.failure_path)
|
52
|
-
FileUtils.mv(compare_to_image, snapshot.new_file, force: true)
|
33
|
+
def build_comparer
|
34
|
+
if element.is_a?(Capybara::Session)
|
35
|
+
DotDiff::Comparible::PageComparer.new(snapshot, nil)
|
36
|
+
elsif element.is_a?(Capybara::Node::Base)
|
37
|
+
DotDiff::Comparible::ElementComparer.new(snapshot, ElementMeta.new(page, element))
|
53
38
|
end
|
54
|
-
|
55
|
-
[result.passed?, result.message]
|
56
39
|
end
|
57
40
|
end
|
58
41
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DotDiff
|
4
|
+
module Comparible
|
5
|
+
class Base
|
6
|
+
def initialize(snapshot, element_meta)
|
7
|
+
@snapshot = snapshot
|
8
|
+
@element_meta = element_meta
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.run(snapshot, element_meta)
|
12
|
+
new(snapshot, element_meta).run
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :snapshot, :element_meta
|
18
|
+
|
19
|
+
def compare(compare_to_image)
|
20
|
+
return [false, img_container.dimensions_mismatch_msg] unless img_container.both_images_same_dimensions?
|
21
|
+
|
22
|
+
cmd = CommandWrapper.new
|
23
|
+
cmd.run(snapshot.basefile, new_image_path, snapshot.diff_file)
|
24
|
+
return [cmd.passed?, cmd.message] if cmd.failed?
|
25
|
+
|
26
|
+
calculate_result(cmd.pixels)
|
27
|
+
end
|
28
|
+
|
29
|
+
def calculate_result(diff_pixels)
|
30
|
+
calc = DotDiff::ThresholdCalculator.new(
|
31
|
+
DotDiff.pixel_threshold,
|
32
|
+
img_container.total_pixels,
|
33
|
+
diff_pixels
|
34
|
+
)
|
35
|
+
|
36
|
+
[calc.under_threshold?, calc.message]
|
37
|
+
end
|
38
|
+
|
39
|
+
def img_container
|
40
|
+
@img_container ||= DotDiff::Image::Container.new(
|
41
|
+
snapshot.basefile,
|
42
|
+
new_image_path
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DotDiff
|
4
|
+
module Comparible
|
5
|
+
class ElementComparer < Base
|
6
|
+
def run
|
7
|
+
take_snapshot_and_crop
|
8
|
+
|
9
|
+
if !File.exist?(snapshot.basefile)
|
10
|
+
snapshot.resave_cropped_file
|
11
|
+
[true, snapshot.basefile]
|
12
|
+
else
|
13
|
+
compare(snapshot.cropped_file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def new_image_path
|
18
|
+
snapshot.cropped_file
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def take_snapshot_and_crop
|
24
|
+
snapshot.capture_from_browser(DotDiff.hide_elements_on_non_full_screen_screenshot)
|
25
|
+
snapshot.crop_and_resave(element_meta)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DotDiff
|
4
|
+
module Comparible
|
5
|
+
class PageComparer < Base
|
6
|
+
def run
|
7
|
+
snapshot.capture_from_browser(true)
|
8
|
+
|
9
|
+
if !File.exist?(snapshot.basefile)
|
10
|
+
snapshot.resave_fullscreen_file
|
11
|
+
[true, snapshot.basefile]
|
12
|
+
else
|
13
|
+
compare(snapshot.fullscreen_file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def new_image_path
|
18
|
+
snapshot.fullscreen_file
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DotDiff
|
2
4
|
class ElementHandler
|
3
5
|
attr_accessor :driver
|
@@ -9,27 +11,28 @@ module DotDiff
|
|
9
11
|
|
10
12
|
def hide
|
11
13
|
elements.each do |xpath|
|
12
|
-
|
13
|
-
driver.execute_script("#{js_element(xpath)}.style.visibility = 'hidden'")
|
14
|
-
end
|
14
|
+
driver.execute_script(script(xpath, :hidden))
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
def show
|
19
19
|
elements.each do |xpath|
|
20
|
-
|
21
|
-
driver.execute_script("#{js_element(xpath)}.style.visibility = ''")
|
22
|
-
end
|
20
|
+
driver.execute_script(script(xpath, :''))
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
def script(xpath, visibility)
|
25
|
+
xpath += if visibility == :hidden
|
26
|
+
"[not(contains(@style, 'visibility'))]"
|
27
|
+
else
|
28
|
+
"[contains(@style, 'visibility: hidden')]"
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
# this is done like so instead of a single pass over all elements due to a bug in Firefox:
|
32
|
+
# https://greasyfork.org/en/forum/discussion/12223/xpath-iteratenext-fails-in-firefox
|
33
|
+
"var elem; while (elem = document.evaluate(\"#{xpath}\", document, "\
|
34
|
+
'null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null).iterateNext()) '\
|
35
|
+
"{ elem.style.visibility = '#{visibility}'; }"
|
33
36
|
end
|
34
37
|
|
35
38
|
def elements
|
data/lib/dotdiff/element_meta.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DotDiff
|
2
4
|
class ElementMeta
|
3
5
|
attr_reader :page, :element_xpath
|
@@ -19,22 +21,26 @@ module DotDiff
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def method_missing(name, *args, &block)
|
22
|
-
if %w
|
24
|
+
if %w[x y width height].include?(name.to_s)
|
23
25
|
case name
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
when :x then rect['left']
|
27
|
+
when :y then rect['top']
|
28
|
+
else rect[name.to_s]
|
27
29
|
end
|
28
30
|
else
|
29
31
|
super
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
35
|
+
def respond_to_missing?(name, _include_private = false)
|
36
|
+
%w[x y width height].include?(name.to_s) || super
|
37
|
+
end
|
38
|
+
|
33
39
|
private
|
34
40
|
|
35
41
|
def js_query(xpath)
|
36
42
|
"document.evaluate(\"#{xpath}\", document, null, XPathResult."\
|
37
|
-
|
43
|
+
'FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect()'
|
38
44
|
end
|
39
45
|
|
40
46
|
def get_rect(page, xpath)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DotDiff
|
4
|
+
module Image
|
5
|
+
class Container
|
6
|
+
def initialize(baseimg_file, newimg_file)
|
7
|
+
@baseimg_file = baseimg_file
|
8
|
+
@newimg_file = newimg_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def both_images_same_dimensions?
|
12
|
+
base_image.rows == new_image.rows &&
|
13
|
+
base_image.columns == new_image.columns
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_pixels
|
17
|
+
base_image.rows * base_image.columns
|
18
|
+
end
|
19
|
+
|
20
|
+
def dimensions_mismatch_msg
|
21
|
+
<<~MSG
|
22
|
+
Images are not the same dimensions to be compared
|
23
|
+
Base file: #{base_image.columns}x#{base_image.rows}
|
24
|
+
New file: #{new_image.columns}x#{new_image.rows}
|
25
|
+
MSG
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :baseimg_file, :newimg_file
|
31
|
+
|
32
|
+
def base_image
|
33
|
+
@base_image ||= Magick::Image.read(baseimg_file).first
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_image
|
37
|
+
@new_image ||= Magick::Image.read(newimg_file).first
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rmagick'
|
2
4
|
|
3
5
|
module DotDiff
|
@@ -6,8 +8,8 @@ module DotDiff
|
|
6
8
|
def crop_and_resave(element)
|
7
9
|
image = load_image(fullscreen_file)
|
8
10
|
image.crop!(
|
9
|
-
element.rectangle.x,
|
10
|
-
element.rectangle.y,
|
11
|
+
element.rectangle.x.floor,
|
12
|
+
element.rectangle.y.floor,
|
11
13
|
width(element, image),
|
12
14
|
height(element, image)
|
13
15
|
)
|
data/lib/dotdiff/snapshot.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DotDiff
|
2
4
|
class Snapshot
|
3
5
|
include Image::Cropper
|
4
6
|
|
5
|
-
attr_reader :
|
7
|
+
attr_reader :subdir, :rootdir, :page, :use_custom_screenshot
|
6
8
|
|
7
|
-
IMAGE_EXT = 'png'
|
9
|
+
IMAGE_EXT = 'png'
|
8
10
|
|
9
11
|
def initialize(options = {})
|
10
12
|
opts = { rootdir: DotDiff.image_store_path }.merge(Hash(options))
|
@@ -12,23 +14,23 @@ module DotDiff
|
|
12
14
|
@subdir = opts[:subdir].to_s
|
13
15
|
@rootdir = opts[:rootdir].to_s
|
14
16
|
@page = opts[:page]
|
15
|
-
@
|
17
|
+
@fullscreen_file = opts[:fullscreen_file]
|
16
18
|
@use_custom_screenshot = opts[:use_custom_screenshot]
|
17
19
|
end
|
18
20
|
|
19
21
|
def fullscreen_file
|
20
|
-
@
|
22
|
+
@fullscreen_file ||= File.join(Dir.tmpdir, subdir, base_filename)
|
21
23
|
end
|
22
24
|
|
23
25
|
def cropped_file
|
24
|
-
@
|
26
|
+
@cropped_file ||= File.join(Dir.tmpdir, subdir, "#{base_filename(false)}_cropped.#{IMAGE_EXT}")
|
25
27
|
end
|
26
28
|
|
27
29
|
def basefile
|
28
30
|
File.join(rootdir, subdir.to_s, base_filename)
|
29
31
|
end
|
30
32
|
|
31
|
-
def base_filename(with_extension=true)
|
33
|
+
def base_filename(with_extension = true)
|
32
34
|
filename = File.basename(@base_filename)
|
33
35
|
extension = File.extname(filename)
|
34
36
|
rtn_file = @base_filename
|
@@ -47,17 +49,19 @@ module DotDiff
|
|
47
49
|
end
|
48
50
|
|
49
51
|
def new_file
|
50
|
-
File.join(failure_path, "#{base_filename(false)}.new.#{IMAGE_EXT}"
|
52
|
+
File.join(failure_path, "#{base_filename(false)}.new.#{IMAGE_EXT}")
|
51
53
|
end
|
52
54
|
|
53
55
|
def diff_file
|
54
56
|
File.join(failure_path, "#{base_filename(false)}.diff.#{IMAGE_EXT}")
|
55
57
|
end
|
56
58
|
|
57
|
-
def capture_from_browser(hide_and_show
|
59
|
+
def capture_from_browser(hide_and_show)
|
58
60
|
return fullscreen_file if use_custom_screenshot
|
59
61
|
|
60
62
|
if hide_and_show
|
63
|
+
element_handler = ElementHandler.new(page)
|
64
|
+
|
61
65
|
element_handler.hide
|
62
66
|
page.save_screenshot(fullscreen_file)
|
63
67
|
element_handler.show
|
@@ -79,10 +83,10 @@ module DotDiff
|
|
79
83
|
def resave_base_file(version)
|
80
84
|
FileUtils.mkdir_p(File.join(DotDiff.image_store_path, subdir))
|
81
85
|
|
82
|
-
if !File.
|
83
|
-
FileUtils.mv(
|
86
|
+
if !File.exist?(basefile) || DotDiff.overwrite_on_resave
|
87
|
+
FileUtils.mv(send("#{version}_file"), basefile, force: true)
|
84
88
|
else
|
85
|
-
FileUtils.mv(
|
89
|
+
FileUtils.mv(send("#{version}_file"), "#{basefile}.r2", force: true)
|
86
90
|
end
|
87
91
|
end
|
88
92
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DotDiff
|
4
|
+
class ThresholdCalculator
|
5
|
+
PIXEL = 'pixel'
|
6
|
+
PERCENT = 'percent'
|
7
|
+
|
8
|
+
def initialize(threshold_config, total_pixels, pixel_diff)
|
9
|
+
@threshold_config = threshold_config
|
10
|
+
@total_pixels = total_pixels
|
11
|
+
@pixel_diff = pixel_diff
|
12
|
+
end
|
13
|
+
|
14
|
+
def under_threshold?
|
15
|
+
return false if total_pixels.nil? || pixel_diff.nil?
|
16
|
+
|
17
|
+
case threshold_type
|
18
|
+
when PIXEL
|
19
|
+
@value = pixel_diff
|
20
|
+
when PERCENT
|
21
|
+
@value = pixel_diff / total_pixels.to_f
|
22
|
+
else
|
23
|
+
raise UnknownTypeError, "Unable to handle threshold type: #{threshold_type}"
|
24
|
+
end
|
25
|
+
|
26
|
+
value <= threshold_value
|
27
|
+
end
|
28
|
+
|
29
|
+
def message
|
30
|
+
"Outcome was '#{value}' difference for type '#{threshold_type}'"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :threshold_config, :pixel_diff, :total_pixels, :value
|
36
|
+
|
37
|
+
def threshold_value
|
38
|
+
return threshold_config if threshold_config.class == Integer
|
39
|
+
|
40
|
+
threshold_config[:value]
|
41
|
+
end
|
42
|
+
|
43
|
+
def threshold_type
|
44
|
+
return PIXEL if threshold_config.class != Hash
|
45
|
+
|
46
|
+
threshold_config[:type].to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/dotdiff/version.rb
CHANGED
metadata
CHANGED
@@ -1,96 +1,98 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dotdiff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Normington
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rmagick
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.15'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.15'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: capybara
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '2.6'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '2.6'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 12.3.3
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 12.3.3
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rspec
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '3.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
83
|
-
description:
|
84
|
-
|
85
|
-
|
82
|
+
version: '3.0'
|
83
|
+
description: Image regression wrapper for Capybara and RSpec using imagemagick supporting
|
84
|
+
both MRI and JRuby versions which supports snap shoting both full page andspecific
|
85
|
+
elements on a page where text checks isn't enough
|
86
86
|
email:
|
87
87
|
- jnormington@users.noreply.github.com
|
88
88
|
executables: []
|
89
89
|
extensions: []
|
90
90
|
extra_rdoc_files: []
|
91
91
|
files:
|
92
|
-
- .gitignore
|
93
|
-
- .rspec
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rspec"
|
94
|
+
- ".rubocop.yml"
|
95
|
+
- ".rubocop_todo.yml"
|
94
96
|
- Gemfile
|
95
97
|
- LICENSE
|
96
98
|
- README.md
|
@@ -101,34 +103,39 @@ files:
|
|
101
103
|
- lib/dotdiff.rb
|
102
104
|
- lib/dotdiff/command_wrapper.rb
|
103
105
|
- lib/dotdiff/comparer.rb
|
106
|
+
- lib/dotdiff/comparible/base.rb
|
107
|
+
- lib/dotdiff/comparible/element_comparer.rb
|
108
|
+
- lib/dotdiff/comparible/page_comparer.rb
|
104
109
|
- lib/dotdiff/element_handler.rb
|
105
110
|
- lib/dotdiff/element_meta.rb
|
111
|
+
- lib/dotdiff/image/container.rb
|
106
112
|
- lib/dotdiff/image/cropper.rb
|
107
113
|
- lib/dotdiff/rspec_matcher.rb
|
108
114
|
- lib/dotdiff/snapshot.rb
|
115
|
+
- lib/dotdiff/threshold_calculator.rb
|
109
116
|
- lib/dotdiff/version.rb
|
110
117
|
homepage: https://github.com/jnormington/dotdiff
|
111
118
|
licenses:
|
112
119
|
- MIT
|
113
120
|
metadata: {}
|
114
|
-
post_install_message:
|
121
|
+
post_install_message:
|
115
122
|
rdoc_options: []
|
116
123
|
require_paths:
|
117
124
|
- lib
|
118
125
|
required_ruby_version: !ruby/object:Gem::Requirement
|
119
126
|
requirements:
|
120
|
-
- -
|
127
|
+
- - ">="
|
121
128
|
- !ruby/object:Gem::Version
|
122
129
|
version: '0'
|
123
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
131
|
requirements:
|
125
|
-
- -
|
132
|
+
- - ">="
|
126
133
|
- !ruby/object:Gem::Version
|
127
134
|
version: '0'
|
128
135
|
requirements: []
|
129
|
-
|
130
|
-
|
131
|
-
signing_key:
|
136
|
+
rubygems_version: 3.1.2
|
137
|
+
signing_key:
|
132
138
|
specification_version: 4
|
133
|
-
summary:
|
139
|
+
summary: Image regression wrapper for Capybara and RSpec using imagemagick supporting
|
140
|
+
both MRI and JRuby versions
|
134
141
|
test_files: []
|