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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 88c24a6cd7b8664be57c3969cde582e2cac5c25c
4
- data.tar.gz: 94542a6d703ee8fb50dcee720c116d2dad4a8cd0
2
+ SHA256:
3
+ metadata.gz: 161e2aca53c1b747a6de45cd09ac975f56938d246e32886f7c7efc65cc7f0426
4
+ data.tar.gz: fc2878dc821b64da63222f2c5b8eb4bfb2f0b6a33d625556165d834d7c386f82
5
5
  SHA512:
6
- metadata.gz: cc886b3a9d07766a64b0b7ee564447fbe629c37402e344c9138707ceabdc80110d62abc1d3b32138e9fc3b7d92e9dbb20089da0a93f60fbf7dc784cb978d0e86
7
- data.tar.gz: ca193151f90733ab22239d964798f6c34f165fb441d8201fba3f3e2d5700f3b817777108e4359771e4ef9a86b18f896612846cff1ed118cd9b5420b573337e39
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in dotdiff.gemspec
data/README.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # Dotdiff
2
2
 
3
- Dotdiff is a very basic wrapper around [perceptual-diff](http://pdiff.sourceforge.net/) which works with both Capybara and RSpec to capture and compare the images with a simple rspec matcher.
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 capybara finders - which once the element is found will query the browser for its dimensions and placement on the page and use that metadata to crop using chunky_png from a full page snapshot.
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 different display suchas username or user specific details, but only for full page screenshots.
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 perceptualdiff binary which is available via apt-get and brew or via http://pdiff.sourceforge.net/
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.perceptual_diff_bin = `which perceptualdiff`.strip
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
- | perceptual_diff_bin | Location of the perceptual diff binary file | `which perceptualdiff`.strip | N/A | Yes |
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
- | max_wait_time | This is similar to the Capybara#default_max_wait_time if you have a high value such as 10, as its possible that the global xpath_elements_to_hide might not always exist it will wait the full time - therefore you can drop it for the hiding and showing of the elements. In this example it would wait up 20 seconds in total 10 for hiding and 10 seconds for re-showing - that is if the element isn't even going to be present on the page. | 2 | Capybara#default_max_wait_time | 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 |
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
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "dotdiff"
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 "irb"
14
+ require 'irb'
14
15
  IRB.start
data/dotdiff.gemspec CHANGED
@@ -1,29 +1,38 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = "dotdiff"
10
+ spec.name = 'dotdiff'
8
11
  spec.version = DotDiff::VERSION
9
- spec.authors = ["Jon Normington"]
10
- spec.email = ["jnormington@users.noreply.github.com"]
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 = "Preceptual diff wrapper for capybara and rspec image regression specs"
13
- spec.description = [spec.summary, "which is great for graphs and charts where checking"\
14
- "the DOM is either impossible to not worth it."].join(' ')
15
- spec.homepage = "https://github.com/jnormington/dotdiff"
16
- spec.license = "MIT"
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 = "exe"
24
+ spec.bindir = 'exe'
20
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
- spec.require_paths = ["lib"]
26
+ spec.require_paths = ['lib']
22
27
 
23
- spec.add_runtime_dependency "rmagick", '~> 2.15'
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 "bundler", "~> 1.11"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
28
- spec.add_development_dependency "capybara", "~> 2.6"
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
- class << self
17
- attr_accessor :perceptual_diff_bin, :resave_base_image, :failure_image_path,
18
- :image_store_path, :overwrite_on_resave, :xpath_elements_to_hide,
19
- :max_wait_time
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
- def configure
22
- yield self
23
- end
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
- def resave_base_image
26
- @resave_base_image ||= false
27
- end
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
- def xpath_elements_to_hide
30
- @xpath_elements_to_hide ||= []
31
- end
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
- def max_wait_time
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
- @ran_checks = true
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
- if output.include?('PASS:')
13
+ begin
14
+ @pixels = Float(output)
13
15
  @failed = false
14
- else
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
- @ran_checks && @failed
26
- end
27
-
28
- def ran_checks
29
- @ran_checks
26
+ @failed
30
27
  end
31
28
 
32
29
  private
33
30
 
34
- def command(base_image, new_image)
35
- "#{DotDiff.perceptual_diff_bin} #{Shellwords.escape(base_image)} "\
36
- "#{Shellwords.escape(new_image)} -verbose"
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
@@ -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
- compare_page
15
+ DotDiff::Comparible::PageComparer.run(snapshot, nil)
14
16
  elsif element.is_a?(Capybara::Node::Base)
15
- compare_element
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
- if element_exists?(xpath)
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
- if element_exists?(xpath)
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 js_element(xpath)
27
- "document.evaluate(\"#{xpath}\", "\
28
- "document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue"
29
- end
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
- def element_exists?(xpath)
32
- driver.find(:xpath, xpath, wait: DotDiff.max_wait_time, visible: :all) rescue nil
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
@@ -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(x y width height).include?(name.to_s)
24
+ if %w[x y width height].include?(name.to_s)
23
25
  case name
24
- when :x then rect['left']
25
- when :y then rect['top']
26
- else rect[name.to_s]
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
- "FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect()"
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
  )
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/expectations'
2
4
 
3
5
  RSpec::Matchers.define :match_image do |filename, opts = {}|
@@ -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 :base_filename, :subdir, :rootdir, :page
7
+ attr_reader :subdir, :rootdir, :page, :use_custom_screenshot
6
8
 
7
- IMAGE_EXT = 'png'.freeze
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
- @fullscreen ||= File.join(Dir.tmpdir, subdir, base_filename)
22
+ @fullscreen_file ||= File.join(Dir.tmpdir, subdir, base_filename)
19
23
  end
20
24
 
21
25
  def cropped_file
22
- @cropped ||= File.join(Dir.tmpdir, subdir, "#{base_filename(false)}_cropped.#{IMAGE_EXT}")
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 failure_file
48
- File.join(failure_path, "#{base_filename(false)}.new.#{IMAGE_EXT}" )
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 = true, element_handler = ElementHandler.new(page))
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.exists?(basefile) || DotDiff.overwrite_on_resave
75
- FileUtils.mv(self.send("#{version}_file"), basefile, force: true)
86
+ if !File.exist?(basefile) || DotDiff.overwrite_on_resave
87
+ FileUtils.mv(send("#{version}_file"), basefile, force: true)
76
88
  else
77
- FileUtils.mv(self.send("#{version}_file"), "#{basefile}.r2", force: true)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DotDiff
2
- VERSION = "1.2.1"
4
+ VERSION = '3.0.0'
3
5
  end
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: 1.2.1
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: 2016-09-12 00:00:00.000000000 Z
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: '1.11'
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: '1.11'
40
+ version: '2'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: capybara
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
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: '10.0'
54
+ version: '2.6'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '3.0'
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: '3.0'
68
+ version: 12.3.3
69
69
  - !ruby/object:Gem::Dependency
70
- name: capybara
70
+ name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ~>
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '2.6'
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: '2.6'
83
- description: Preceptual diff wrapper for capybara and rspec image regression specs
84
- which is great for graphs and charts where checkingthe DOM is either impossible
85
- to not worth it.
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
- rubyforge_project:
130
- rubygems_version: 2.0.14.1
131
- signing_key:
136
+ rubygems_version: 3.1.2
137
+ signing_key:
132
138
  specification_version: 4
133
- summary: Preceptual diff wrapper for capybara and rspec image regression specs
139
+ summary: Image regression wrapper for Capybara and RSpec using imagemagick supporting
140
+ both MRI and JRuby versions
134
141
  test_files: []