dotdiff 1.2.1 → 3.0.0
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 +19 -8
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/dotdiff.gemspec +26 -17
- data/lib/dotdiff.rb +61 -15
- data/lib/dotdiff/command_wrapper.rb +18 -16
- data/lib/dotdiff/comparer.rb +4 -39
- data/lib/dotdiff/comparible/base.rb +56 -0
- data/lib/dotdiff/comparible/element_comparer.rb +25 -0
- data/lib/dotdiff/comparible/page_comparer.rb +18 -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 +24 -12
- 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: 161e2aca53c1b747a6de45cd09ac975f56938d246e32886f7c7efc65cc7f0426
|
4
|
+
data.tar.gz: fc2878dc821b64da63222f2c5b8eb4bfb2f0b6a33d625556165d834d7c386f82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b130dcea90f5dc4ca7d85d1154fe84e7209554b4b4b3e09d617e32ec858c256a3bc489e6ffb4e97b738bb02092e95ca225707940be4990f1be5889b50a77043
|
7
|
+
data.tar.gz: 5acf38c5ee36c8964041fd1e9b6c13dfdce58ecf15e0e3ae48b2188326c06f654f5c413d4564bdb63009d9bccc4607813d99b711f3966c02b12d36b2d9228dff
|
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
@@ -1,15 +1,22 @@
|
|
1
1
|
# Dotdiff
|
2
2
|
|
3
|
-
Dotdiff is a very basic wrapper around
|
3
|
+
Dotdiff is a very basic wrapper around imåge magick compare program which works with both Capybara
|
4
|
+
and RSpec to capture and compare the images with a simple rspec matcher.
|
4
5
|
|
5
|
-
It is now also possible to snapshot a particular element on the page just by using the standard
|
6
|
+
It is now also possible to snapshot a particular element on the page just by using the standard
|
7
|
+
capybara finders - which once the element is found will query the browser for its dimensions and
|
8
|
+
placement on the page and use that metadata to crop using chunky_png from a full page snapshot.
|
6
9
|
|
7
|
-
It can also hide certain elements via executing javascript for elements which can change with
|
10
|
+
It can also hide certain elements via executing javascript for elements which can change with
|
11
|
+
different display suchas username or user specific details, but only for full page screenshots.
|
8
12
|
|
9
13
|
## Installation
|
10
14
|
|
15
|
+
|
11
16
|
Add this line to your application's Gemfile:
|
12
17
|
|
18
|
+
This is the same for JRuby platform as well (both MRI and JRuby versions of gem is uploaded)
|
19
|
+
|
13
20
|
```ruby
|
14
21
|
gem 'dotdiff'
|
15
22
|
```
|
@@ -26,7 +33,7 @@ Or install it yourself as:
|
|
26
33
|
## Usage
|
27
34
|
|
28
35
|
### Dependencies
|
29
|
-
First ensure to install
|
36
|
+
First ensure to install image magick binary which is available via apt-get or brew
|
30
37
|
|
31
38
|
In your spec/spec_helper
|
32
39
|
```
|
@@ -43,11 +50,13 @@ In an initializer you can configure certain options example shown below within D
|
|
43
50
|
|
44
51
|
```ruby
|
45
52
|
DotDiff.configure do |config|
|
46
|
-
config.
|
53
|
+
config.image_magick_diff_bin = `which compare`
|
54
|
+
config.pixel_threshold = { type: 'percent', value: 0.04 }
|
55
|
+
config.image_magick_options = '-metric AE'
|
47
56
|
config.image_store_path = File.join('/home', 'user', 'images')
|
48
57
|
config.xpath_elements_to_hide = ["id('main')"]
|
58
|
+
config.hide_elements_on_non_full_screen_screenshot = true
|
49
59
|
config.failure_image_path = File.join('/home', 'user', 'failure_comparisions')
|
50
|
-
config.max_wait_time = 2
|
51
60
|
end
|
52
61
|
```
|
53
62
|
|
@@ -71,11 +80,13 @@ The only difference for the element specific is passing a specific element in th
|
|
71
80
|
|
72
81
|
| Config | Description | Example | Default | Required |
|
73
82
|
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|--------------------------------|----------|
|
74
|
-
|
|
83
|
+
| image_magick_diff_bin | Location of the image magick compare binary file | `which compare` | N/A | Yes |
|
75
84
|
| image_store_path | The root path to store the base images | File.join('/home', 'user','images') | nil | Yes |
|
76
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 |
|
77
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 |
|
78
|
-
|
|
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 |
|
89
|
+
| image_magick_options | This allows you to pass some custom options to image magick | '-fuzz 10% -metric RSME' | '-fuzz 5% -metric AE' | No |
|
79
90
|
|
80
91
|
## Contributing
|
81
92
|
|
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,29 +11,73 @@ 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
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
26
|
+
class UnknownTypeError < StandardError; end
|
27
|
+
class InvalidValueError < StandardError; end
|
28
|
+
|
29
|
+
SUPPORTED_THRESHOLD_TYPES = %w[pixel percent].freeze
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :failure_image_path, :image_store_path, :overwrite_on_resave
|
33
|
+
|
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
|
36
|
+
|
37
|
+
def configure
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
|
41
|
+
def resave_base_image
|
42
|
+
@resave_base_image ||= false
|
43
|
+
end
|
44
|
+
|
45
|
+
def xpath_elements_to_hide
|
46
|
+
@xpath_elements_to_hide ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
def hide_elements_on_non_full_screen_screenshot
|
50
|
+
@hide_elements_on_non_full_screen_screenshot ||= false
|
51
|
+
end
|
52
|
+
|
53
|
+
def image_magick_options
|
54
|
+
@image_magick_options ||= '-fuzz 5% -metric AE'
|
55
|
+
end
|
56
|
+
|
57
|
+
def image_magick_diff_bin
|
58
|
+
@image_magick_diff_bin.to_s.strip
|
59
|
+
end
|
60
|
+
|
61
|
+
def pixel_threshold
|
62
|
+
@pixel_threshold ||= { type: 'pixel', value: 100 }
|
63
|
+
end
|
20
64
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
71
|
|
25
|
-
|
26
|
-
|
27
|
-
|
72
|
+
unless SUPPORTED_THRESHOLD_TYPES.include?(config.fetch(:type))
|
73
|
+
raise UnknownTypeError, "Unknown threshold type supports only: #{SUPPORTED_THRESHOLD_TYPES.join(',')}"
|
74
|
+
end
|
28
75
|
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
32
79
|
|
33
|
-
|
34
|
-
@max_wait_time || Capybara.default_max_wait_time
|
80
|
+
@pixel_threshold = config
|
35
81
|
end
|
36
82
|
end
|
37
83
|
end
|
@@ -1,19 +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
|
6
|
-
|
7
|
-
def run(base_image, new_image)
|
8
|
-
output = `#{command(base_image, new_image)}`
|
7
|
+
attr_reader :message, :pixels
|
9
8
|
|
10
|
-
|
9
|
+
def run(base_image, new_image, diff_image_path)
|
10
|
+
output = run_command(base_image, new_image, diff_image_path)
|
11
|
+
@message = output
|
11
12
|
|
12
|
-
|
13
|
+
begin
|
14
|
+
@pixels = Float(output)
|
13
15
|
@failed = false
|
14
|
-
|
16
|
+
rescue ArgumentError
|
15
17
|
@failed = true
|
16
|
-
@message = output.split("\n").join(' ')
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
@@ -22,18 +23,19 @@ module DotDiff
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def failed?
|
25
|
-
@
|
26
|
-
end
|
27
|
-
|
28
|
-
def ran_checks
|
29
|
-
@ran_checks
|
26
|
+
@failed
|
30
27
|
end
|
31
28
|
|
32
29
|
private
|
33
30
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
31
|
+
def run_command(base_image, new_image, diff_image_path)
|
32
|
+
`#{command(base_image, new_image, diff_image_path)}`.strip
|
33
|
+
end
|
34
|
+
|
35
|
+
def command(base_image, new_image, diff_image_path)
|
36
|
+
"#{DotDiff.image_magick_diff_bin} #{DotDiff.image_magick_options} " \
|
37
|
+
"#{Shellwords.escape(base_image)} #{Shellwords.escape(new_image)} " \
|
38
|
+
"#{Shellwords.escape(diff_image_path)} 2>&1"
|
37
39
|
end
|
38
40
|
end
|
39
41
|
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
|
@@ -10,49 +12,12 @@ module DotDiff
|
|
10
12
|
|
11
13
|
def result
|
12
14
|
if element.is_a?(Capybara::Session)
|
13
|
-
|
15
|
+
DotDiff::Comparible::PageComparer.run(snapshot, nil)
|
14
16
|
elsif element.is_a?(Capybara::Node::Base)
|
15
|
-
|
17
|
+
DotDiff::Comparible::ElementComparer.run(snapshot, ElementMeta.new(page, element))
|
16
18
|
else
|
17
19
|
raise ArgumentError, "Unknown element class received: #{element.class.name}"
|
18
20
|
end
|
19
21
|
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def compare_element(element_meta = ElementMeta.new(page, element))
|
24
|
-
snapshot.capture_from_browser(false, nil)
|
25
|
-
snapshot.crop_and_resave(element_meta)
|
26
|
-
|
27
|
-
if !File.exists?(snapshot.basefile)
|
28
|
-
snapshot.resave_cropped_file
|
29
|
-
[true, snapshot.basefile]
|
30
|
-
else
|
31
|
-
compare(snapshot.cropped_file)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def compare_page
|
36
|
-
snapshot.capture_from_browser
|
37
|
-
|
38
|
-
if !File.exists?(snapshot.basefile)
|
39
|
-
snapshot.resave_fullscreen_file
|
40
|
-
[true, snapshot.basefile]
|
41
|
-
else
|
42
|
-
compare(snapshot.fullscreen_file)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def compare(compare_to_image)
|
47
|
-
result = CommandWrapper.new
|
48
|
-
result.run(snapshot.basefile, compare_to_image)
|
49
|
-
|
50
|
-
if result.failed? && DotDiff.failure_image_path
|
51
|
-
FileUtils.mkdir_p(snapshot.failure_path)
|
52
|
-
FileUtils.mv(compare_to_image, snapshot.failure_file, force: true)
|
53
|
-
end
|
54
|
-
|
55
|
-
[result.passed?, result.message]
|
56
|
-
end
|
57
22
|
end
|
58
23
|
end
|
@@ -0,0 +1,56 @@
|
|
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, :new_image
|
18
|
+
|
19
|
+
def compare(compare_to_image)
|
20
|
+
@new_image = compare_to_image
|
21
|
+
return [false, img_container.dimensions_mismatch_msg] unless img_container.both_images_same_dimensions?
|
22
|
+
|
23
|
+
cmd = CommandWrapper.new
|
24
|
+
cmd.run(snapshot.basefile, new_image, snapshot.diff_file)
|
25
|
+
return [cmd.passed?, cmd.message] if cmd.failed?
|
26
|
+
|
27
|
+
calculate_result(cmd.pixels)
|
28
|
+
end
|
29
|
+
|
30
|
+
def calculate_result(diff_pixels)
|
31
|
+
calc = DotDiff::ThresholdCalculator.new(
|
32
|
+
DotDiff.pixel_threshold,
|
33
|
+
img_container.total_pixels,
|
34
|
+
diff_pixels
|
35
|
+
)
|
36
|
+
|
37
|
+
passed = calc.under_threshold?
|
38
|
+
write_failure_imgs if !passed && DotDiff.failure_image_path
|
39
|
+
|
40
|
+
[passed, calc.message]
|
41
|
+
end
|
42
|
+
|
43
|
+
def img_container
|
44
|
+
@img_container ||= DotDiff::Image::Container.new(
|
45
|
+
snapshot.basefile,
|
46
|
+
new_image
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_failure_imgs
|
51
|
+
FileUtils.mkdir_p(snapshot.failure_path)
|
52
|
+
FileUtils.mv(new_image, snapshot.new_file, force: true)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
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
|
+
private
|
18
|
+
|
19
|
+
def take_snapshot_and_crop
|
20
|
+
snapshot.capture_from_browser(DotDiff.hide_elements_on_non_full_screen_screenshot)
|
21
|
+
snapshot.crop_and_resave(element_meta)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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
|
+
end
|
17
|
+
end
|
18
|
+
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,21 +14,23 @@ module DotDiff
|
|
12
14
|
@subdir = opts[:subdir].to_s
|
13
15
|
@rootdir = opts[:rootdir].to_s
|
14
16
|
@page = opts[:page]
|
17
|
+
@fullscreen_file = opts[:fullscreen_file]
|
18
|
+
@use_custom_screenshot = opts[:use_custom_screenshot]
|
15
19
|
end
|
16
20
|
|
17
21
|
def fullscreen_file
|
18
|
-
@
|
22
|
+
@fullscreen_file ||= File.join(Dir.tmpdir, subdir, base_filename)
|
19
23
|
end
|
20
24
|
|
21
25
|
def cropped_file
|
22
|
-
@
|
26
|
+
@cropped_file ||= File.join(Dir.tmpdir, subdir, "#{base_filename(false)}_cropped.#{IMAGE_EXT}")
|
23
27
|
end
|
24
28
|
|
25
29
|
def basefile
|
26
30
|
File.join(rootdir, subdir.to_s, base_filename)
|
27
31
|
end
|
28
32
|
|
29
|
-
def base_filename(with_extension=true)
|
33
|
+
def base_filename(with_extension = true)
|
30
34
|
filename = File.basename(@base_filename)
|
31
35
|
extension = File.extname(filename)
|
32
36
|
rtn_file = @base_filename
|
@@ -41,15 +45,23 @@ module DotDiff
|
|
41
45
|
end
|
42
46
|
|
43
47
|
def failure_path
|
44
|
-
File.join(DotDiff.failure_image_path, subdir)
|
48
|
+
File.join(DotDiff.failure_image_path.to_s, subdir)
|
49
|
+
end
|
50
|
+
|
51
|
+
def new_file
|
52
|
+
File.join(failure_path, "#{base_filename(false)}.new.#{IMAGE_EXT}")
|
45
53
|
end
|
46
54
|
|
47
|
-
def
|
48
|
-
File.join(failure_path, "#{base_filename(false)}.
|
55
|
+
def diff_file
|
56
|
+
File.join(failure_path, "#{base_filename(false)}.diff.#{IMAGE_EXT}")
|
49
57
|
end
|
50
58
|
|
51
|
-
def capture_from_browser(hide_and_show
|
59
|
+
def capture_from_browser(hide_and_show)
|
60
|
+
return fullscreen_file if use_custom_screenshot
|
61
|
+
|
52
62
|
if hide_and_show
|
63
|
+
element_handler = ElementHandler.new(page)
|
64
|
+
|
53
65
|
element_handler.hide
|
54
66
|
page.save_screenshot(fullscreen_file)
|
55
67
|
element_handler.show
|
@@ -71,10 +83,10 @@ module DotDiff
|
|
71
83
|
def resave_base_file(version)
|
72
84
|
FileUtils.mkdir_p(File.join(DotDiff.image_store_path, subdir))
|
73
85
|
|
74
|
-
if !File.
|
75
|
-
FileUtils.mv(
|
86
|
+
if !File.exist?(basefile) || DotDiff.overwrite_on_resave
|
87
|
+
FileUtils.mv(send("#{version}_file"), basefile, force: true)
|
76
88
|
else
|
77
|
-
FileUtils.mv(
|
89
|
+
FileUtils.mv(send("#{version}_file"), "#{basefile}.r2", force: true)
|
78
90
|
end
|
79
91
|
end
|
80
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.0
|
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-21 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: []
|