dotdiff 0.3.1 → 1.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
2
  SHA1:
3
- metadata.gz: 9997ae1ee2c000b67cfdd6e76abfc3e5cfc28849
4
- data.tar.gz: c35cc13323a21458161535f406b2e7c4af68421a
3
+ metadata.gz: aa3cab17d540380ffe0a575fc885cb38b0094fef
4
+ data.tar.gz: 4bb66612ff6708cc0c4af0ae928cd602e5b29201
5
5
  SHA512:
6
- metadata.gz: 752d3c3960ab4630c43e68c69826caec7da722171e267ec71fc9ce4bd227902e54c73c63aaea33dbd43934dd0a64804c61414d181dceb78bdc68e145ba6fbb43
7
- data.tar.gz: 9e4d4391cda1b47e0163366253c438fdc53f52724f8daf705559dd82c3d0526d7a95cf9fb55905fcb5c99325a6f01d4eb26067476ef460eb737a0d06b2148618
6
+ metadata.gz: 5920c90bfc868e52ee879dbc46da73bb4772832f09ff35ca31e2d50f2fdd02ba47674e239de2c8b135de0461e4049a35ad9c8f36dee6ada47689e8fe933d1e95
7
+ data.tar.gz: 6d20f46f80cbc2c28f6e2906db735fb5114bd5c6af96d8c3ea7d48667eaf7d56aac432c7a664309b30a0a7dc42c7544f08e664c9e6b4d3e08626cd5cda33cdcd
data/README.md CHANGED
@@ -1,6 +1,10 @@
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. Which can also hide certain elements via executing javascript for elements which can change with different display suchas username or user specific details.
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.
4
+
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
+
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.
4
8
 
5
9
  ## Installation
6
10
 
@@ -18,8 +22,10 @@ Or install it yourself as:
18
22
 
19
23
  $ gem install dotdiff
20
24
 
25
+
21
26
  ## Usage
22
27
 
28
+ ### Dependencies
23
29
  First ensure to install perceptualdiff binary which is available via apt-get and brew or via http://pdiff.sourceforge.net/
24
30
 
25
31
  In your spec/spec_helper
@@ -32,50 +38,44 @@ If you want the rspec_matcher require the following as well
32
38
  require 'dotdiff/rspec_matcher'
33
39
  ```
34
40
 
41
+ ### Configuration
35
42
  In an initializer you can configure certain options example shown below within Dotdiff
36
43
 
37
44
  ```ruby
38
45
  DotDiff.configure do |config|
39
46
  config.perceptual_diff_bin = `which perceptualdiff`.strip
40
- config.image_store_path = File.expand_path('../../spec/fixtures/images', __FILE__)
41
- config.js_elements_to_hide = [
42
- "document.getElementsByClassName('drop-menu-toggle')[0]",
43
- "document.getElementsById('username-title')",
44
- ]
45
- config.failure_image_path = '/home/user/spec_image_failures'
46
-
47
+ config.image_store_path = File.join('/home', 'user', 'images')
48
+ config.xpath_elements_to_hide = ["id('main')"]
49
+ config.failure_image_path = File.join('/home', 'user', 'failure_comparisions')
50
+ config.max_wait_time = 2
47
51
  end
48
52
  ```
49
53
 
50
- Basic usage in your spec is just the line below;
54
+ ### Spec usage
55
+
56
+ For a full page screenshot you can use the below;
51
57
 
52
58
  ```ruby
53
- expect(page).to match_image('GooglePage', subdir: 'Google')
59
+ expect(page).to match_image('HomePage', subdir: 'Normitec')
54
60
  ```
55
61
 
56
- It does the following;
62
+ For a specific element screenshot you can use like so;
57
63
 
58
- It builds the base_image_file location with DotDiff.image_store_path + subdir + filename + .png
59
-
60
- It then checks if that file exists;
61
- - If it doesn't exist it or `resave_base_image` is passed;
62
- - it hides any elements defined in `js_elements_to_hide`
63
- - uses `Capybara::Session#save_screenshot` to create a screenshot
64
- - it then un-hides any elements defined as above
65
- - returns that compare was successfull
66
-
67
- Also if resave_base_image is passed and DotDiff.overwrite_on_resave is false it creates an r2 version so that you can compare with the original or if the file is deleted it will just write the same file.
64
+ ```ruby
65
+ expect(find('#login-form')).to match_image('LoginForm', subdir: 'Normitec')
66
+ ```
68
67
 
69
- - If the `base_image_file` exists and resave_base_image is false;
70
- - it hides any elements defined in `js_elements_to_hide`
71
- - uses `Capybara::Session#save_screenshot` to create a screenshot in a tmpdir to compare against the base_image
72
- - runs the command and observes the result
73
- - it then un-hides any elements defined as above
74
- - if failure_image_path is defined it moves the new captured image to that path named as the original file
75
- - returns both the result and stdout failure message
68
+ The only difference for the element specific is passing a specific element in the `expect` parameter.
76
69
 
77
- A failure message might look like the following `expected to match image but failed with: FAIL: Images are 120 pixels visibly different`
70
+ ## Configuration Options
78
71
 
72
+ | Config | Description | Example | Default | Required |
73
+ |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|--------------------------------|----------|
74
+ | perceptual_diff_bin | Location of the perceptual diff binary file | `which perceptualdiff`.strip | N/A | Yes |
75
+ | image_store_path | The root path to store the base images | File.join('/home', 'user','images') | nil | Yes |
76
+ | 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 |
77
+ | 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 |
79
79
 
80
80
  ## Contributing
81
81
 
@@ -86,9 +86,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jnormi
86
86
 
87
87
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
88
88
 
89
- ## Whats left to do
89
+ ## Whats still to do
90
90
 
91
91
  Its not fully completed yet but is usable in its current state.
92
92
  - Improve the message output to extract just the fail line
93
93
  - Add an integration spec
94
- - Support multi browser resolutions and handle in the file name
data/dotdiff.gemspec CHANGED
@@ -20,7 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.add_runtime_dependency "chunky_png", '~> 1.3'
24
+
23
25
  spec.add_development_dependency "bundler", "~> 1.11"
24
26
  spec.add_development_dependency "rake", "~> 10.0"
25
27
  spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "capybara", "~> 2.6"
26
29
  end
@@ -0,0 +1,60 @@
1
+ module DotDiff
2
+ class Comparer
3
+ attr_reader :element, :page, :snapshot
4
+
5
+ def initialize(element, page, snapshot)
6
+ @page = page
7
+ @element = element
8
+ @snapshot = snapshot
9
+ end
10
+
11
+ def result
12
+ if element.is_a?(Capybara::Session)
13
+ compare_page
14
+ elsif element.is_a?(Capybara::Node::Base)
15
+ compare_element
16
+ else
17
+ raise ArgumentError, "Unknown element class received: #{element.class.name}"
18
+ end
19
+ 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
+ failure_path = File.join(DotDiff.failure_image_path, snapshot.subdir)
52
+
53
+ FileUtils.mkdir_p(failure_path)
54
+ FileUtils.mv(compare_to_image, File.join(failure_path, snapshot.base_filename), force: true)
55
+ end
56
+
57
+ [result.passed?, result.message]
58
+ end
59
+ end
60
+ end
@@ -2,7 +2,7 @@ module DotDiff
2
2
  class ElementHandler
3
3
  attr_accessor :driver
4
4
 
5
- def initialize(driver, elements = DotDiff.js_elements_to_hide)
5
+ def initialize(driver, elements = DotDiff.xpath_elements_to_hide)
6
6
  @driver = driver
7
7
  @elements = elements
8
8
  end
@@ -29,8 +29,7 @@ module DotDiff
29
29
  end
30
30
 
31
31
  def element_exists?(xpath)
32
- # TODO: Allow custom overriding beyond Capybara.default_max_wait_time
33
- driver.find(:xpath, xpath, wait: 2, visible: :all) rescue nil
32
+ driver.find(:xpath, xpath, wait: DotDiff.max_wait_time, visible: :all) rescue nil
34
33
  end
35
34
 
36
35
  def elements
@@ -0,0 +1,45 @@
1
+ module DotDiff
2
+ class ElementMeta
3
+ attr_reader :page, :element_xpath
4
+
5
+ def initialize(page, element)
6
+ @element_xpath = element.path
7
+ @page = page
8
+ end
9
+
10
+ def rectangle
11
+ @rectangle ||= Rectangle.new(@page, @element_xpath)
12
+ end
13
+
14
+ class Rectangle
15
+ attr_reader :rect
16
+
17
+ def initialize(page, xpath)
18
+ @rect = get_rect(page, xpath)
19
+ end
20
+
21
+ def method_missing(name, *args, &block)
22
+ if %w(x y width height).include?(name.to_s)
23
+ case name
24
+ when :x then rect['left']
25
+ when :y then rect['top']
26
+ else rect[name.to_s]
27
+ end
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def js_query(xpath)
36
+ "document.evaluate(\"#{xpath}\", document, null, XPathResult."\
37
+ "FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect()"
38
+ end
39
+
40
+ def get_rect(page, xpath)
41
+ page.evaluate_script(js_query(xpath))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ require 'chunky_png'
2
+
3
+ module DotDiff
4
+ module Image
5
+ module Cropper
6
+ def crop_and_resave(element)
7
+ image = load_image(fullscreen_file)
8
+
9
+ image.crop!(
10
+ element.rectangle.x,
11
+ element.rectangle.y,
12
+ width(element, image),
13
+ height(element, image)
14
+ )
15
+
16
+ image.save(cropped_file)
17
+ end
18
+
19
+ def load_image(image_file)
20
+ ::ChunkyPNG::Image.from_file(image_file)
21
+ end
22
+
23
+ def height(element, image)
24
+ element_height = element.rectangle.height + element.rectangle.y
25
+
26
+ if element_height > image.height
27
+ image.height - element.rectangle.y
28
+ else
29
+ element.rectangle.height
30
+ end
31
+ end
32
+
33
+ def width(element, image)
34
+ element_width = element.rectangle.width + element.rectangle.x
35
+
36
+ if element_width > image.width
37
+ image.width - element.rectangle.x
38
+ else
39
+ element.rectangle.width
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,15 +1,17 @@
1
1
  require 'rspec/expectations'
2
2
 
3
3
  RSpec::Matchers.define :match_image do |filename, opts = {}|
4
- match do |page|
5
- image = DotDiff::Image.new(Hash(opts).merge!(file_name: filename, driver: page))
4
+ match do |page_or_element|
5
+ options = Hash(opts).merge(filename: filename, page: page)
6
6
 
7
- result, @message = image.compare
7
+ snapshot = DotDiff::Snapshot.new(options)
8
+ comparer = DotDiff::Comparer.new(page_or_element, page, snapshot)
9
+
10
+ result, @message = comparer.result
8
11
  result == true
9
12
  end
10
13
 
11
-
12
- failure_message do |actual|
13
- "expected to match image but failed with: #{@message}"
14
+ failure_message do |page_or_element|
15
+ "expected #{page_or_element.class} to match image but failed with: #{@message}"
14
16
  end
15
17
  end
@@ -0,0 +1,73 @@
1
+ module DotDiff
2
+ class Snapshot
3
+ include Image::Cropper
4
+
5
+ attr_reader :base_filename, :subdir, :rootdir, :page
6
+
7
+ IMAGE_EXT = 'png'.freeze
8
+
9
+ def initialize(options = {})
10
+ opts = { rootdir: DotDiff.image_store_path }.merge(Hash(options))
11
+ @base_filename = opts[:filename]
12
+ @subdir = opts[:subdir].to_s
13
+ @rootdir = opts[:rootdir].to_s
14
+ @page = opts[:page]
15
+ end
16
+
17
+ def fullscreen_file
18
+ @fullscreen ||= File.join(Dir.tmpdir, subdir, base_filename)
19
+ end
20
+
21
+ def cropped_file
22
+ @cropped ||= File.join(Dir.tmpdir, subdir, "#{base_filename(false)}_cropped.#{IMAGE_EXT}")
23
+ end
24
+
25
+ def basefile
26
+ File.join(rootdir, subdir.to_s, base_filename)
27
+ end
28
+
29
+ def base_filename(with_extention=true)
30
+ filename = File.basename(@base_filename)
31
+ extension = File.extname(filename)
32
+ rtn_file = @base_filename
33
+
34
+ if with_extention
35
+ rtn_file = "#{@base_filename}.#{IMAGE_EXT}" if extension.empty?
36
+ else
37
+ rtn_file = @base_filename.sub(ext, '') unless extension.empty?
38
+ end
39
+
40
+ rtn_file
41
+ end
42
+
43
+ def capture_from_browser(hide_and_show = true, element_handler = ElementHandler.new(page))
44
+ if hide_and_show
45
+ element_handler.hide
46
+ page.save_screenshot(fullscreen_file)
47
+ element_handler.show
48
+ else
49
+ page.save_screenshot(fullscreen_file)
50
+ end
51
+ end
52
+
53
+ def resave_cropped_file
54
+ resave_base_file(:cropped)
55
+ end
56
+
57
+ def resave_fullscreen_file
58
+ resave_base_file(:fullscreen)
59
+ end
60
+
61
+ private
62
+
63
+ def resave_base_file(version)
64
+ FileUtils.mkdir_p(File.join(DotDiff.image_store_path, subdir))
65
+
66
+ if !File.exists?(basefile) || DotDiff.overwrite_on_resave
67
+ FileUtils.mv(self.send("#{version}_file"), basefile, force: true)
68
+ else
69
+ FileUtils.mv(self.send("#{version}_file"), "#{basefile}.r2", force: true)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,3 +1,3 @@
1
1
  module DotDiff
2
- VERSION = "0.3.1"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/dotdiff.rb CHANGED
@@ -2,15 +2,21 @@ require 'dotdiff/version'
2
2
 
3
3
  require 'shellwords'
4
4
  require 'tmpdir'
5
+ require 'fileutils'
5
6
 
6
7
  require 'dotdiff/command_wrapper'
7
- require 'dotdiff/image'
8
8
  require 'dotdiff/element_handler'
9
9
 
10
+ require 'dotdiff/element_meta'
11
+ require 'dotdiff/image/cropper'
12
+ require 'dotdiff/snapshot'
13
+ require 'dotdiff/comparer'
14
+
10
15
  module DotDiff
11
16
  class << self
12
17
  attr_accessor :perceptual_diff_bin, :resave_base_image, :failure_image_path,
13
- :image_store_path, :overwrite_on_resave, :js_elements_to_hide
18
+ :image_store_path, :overwrite_on_resave, :xpath_elements_to_hide,
19
+ :max_wait_time
14
20
 
15
21
  def configure
16
22
  yield self
@@ -20,8 +26,12 @@ module DotDiff
20
26
  @resave_base_image ||= false
21
27
  end
22
28
 
23
- def js_elements_to_hide
24
- @js_elements_to_hide ||= []
29
+ def xpath_elements_to_hide
30
+ @xpath_elements_to_hide ||= []
25
31
  end
26
- end
32
+
33
+ def max_wait_time
34
+ @max_wait_time || Capybara.default_max_wait_time
35
+ end
36
+ end
27
37
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dotdiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Normington
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-03-15 00:00:00.000000000 Z
11
+ date: 2016-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chunky_png
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - ~>
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: capybara
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '2.6'
55
83
  description: Preceptual diff wrapper for capybara and rspec image regression specs
56
84
  which is great for graphs and charts where checkingthe DOM is either impossible
57
85
  to not worth it.
@@ -72,9 +100,12 @@ files:
72
100
  - dotdiff.gemspec
73
101
  - lib/dotdiff.rb
74
102
  - lib/dotdiff/command_wrapper.rb
103
+ - lib/dotdiff/comparer.rb
75
104
  - lib/dotdiff/element_handler.rb
76
- - lib/dotdiff/image.rb
105
+ - lib/dotdiff/element_meta.rb
106
+ - lib/dotdiff/image/cropper.rb
77
107
  - lib/dotdiff/rspec_matcher.rb
108
+ - lib/dotdiff/snapshot.rb
78
109
  - lib/dotdiff/version.rb
79
110
  homepage: https://github.com/jnormington/dotdiff
80
111
  licenses:
data/lib/dotdiff/image.rb DELETED
@@ -1,78 +0,0 @@
1
- require 'fileutils'
2
-
3
- module DotDiff
4
- class Image
5
- attr_accessor :driver, :resave_base_image, :subdir, :file_name
6
-
7
- def initialize(opts = {})
8
- Hash(opts).each do |key, value|
9
- if self.respond_to?("#{key}=")
10
- self.send("#{key}=", value)
11
- end
12
- end
13
- end
14
-
15
- def compare
16
- outcome = []
17
-
18
- if !File.exists?(base_image_file) || resave_base_image
19
- path = capture_and_resave_base_image
20
- outcome = [true, path]
21
- else
22
- compare_to_image = capture_from_browser
23
- result = CommandWrapper.new
24
-
25
- result.run(base_image_file, compare_to_image)
26
-
27
- if result.failed? && DotDiff.failure_image_path
28
- FileUtils.mkdir_p(File.join(DotDiff.failure_image_path, subdir.to_s))
29
- file_name = File.basename(compare_to_image)
30
-
31
- FileUtils.mv(compare_to_image,
32
- File.join(DotDiff.failure_image_path, subdir.to_s, file_name), force: true)
33
- end
34
-
35
- outcome = [result.passed?, result.message]
36
- end
37
-
38
- outcome
39
- end
40
-
41
- def resave_base_image
42
- @resave_base_image.nil? ? DotDiff.resave_base_image : @resave_base_image
43
- end
44
-
45
- def base_image_file
46
- @file_name = "#{file_name}.png" unless file_name.include?('.png')
47
-
48
- File.join(DotDiff.image_store_path, subdir.to_s, file_name)
49
- end
50
-
51
- private
52
-
53
- def element_handler
54
- @elemnt_handler ||= ElementHandler.new(driver)
55
- end
56
-
57
- def capture_from_browser
58
- tmp_screenshot_file = File.join(Dir.tmpdir, File.basename(base_image_file))
59
-
60
- element_handler.hide
61
- driver.save_screenshot(tmp_screenshot_file)
62
- element_handler.show
63
-
64
- tmp_screenshot_file
65
- end
66
-
67
- def capture_and_resave_base_image
68
- tmp_image = capture_from_browser
69
- FileUtils.mkdir_p(File.join(DotDiff.image_store_path, subdir))
70
-
71
- if !File.exists?(base_image_file) || DotDiff.overwrite_on_resave
72
- FileUtils.mv(tmp_image, base_image_file, force: true)
73
- else
74
- FileUtils.mv(tmp_image, "#{base_image_file}.r2", force: true)
75
- end
76
- end
77
- end
78
- end