looks_good 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +18 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +20 -0
- data/README.md +123 -0
- data/lib/looks_good/capture_element.rb +47 -0
- data/lib/looks_good/comparison.rb +74 -0
- data/lib/looks_good/configuration.rb +77 -0
- data/lib/looks_good/image.rb +73 -0
- data/lib/looks_good/matchers/look_like_matcher.rb +22 -0
- data/lib/looks_good/rspec_config.rb +5 -0
- data/lib/looks_good/version.rb +3 -0
- data/lib/looks_good.rb +104 -0
- data/looks_good.gemspec +27 -0
- data/reports/rspec_unit_test_output.html +340 -0
- data/spec/acceptance/gatling_acceptance_spec.rb +83 -0
- data/spec/acceptance/rspec_matcher_spec.rb +32 -0
- data/spec/capture_spec.rb +33 -0
- data/spec/comparison_spec.rb +66 -0
- data/spec/configuration_spec.rb +180 -0
- data/spec/gatling_spec.rb +87 -0
- data/spec/image_spec.rb +108 -0
- data/spec/integration/gatling_integration_spec.rb +36 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +59 -0
- data/spec/support/assets/fruit_app.html +23 -0
- data/temp-1032.rdb +0 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 33bfb614a176498eb6a13180459e40aff96fc328673ab0edd6cbcb2b4b28345f
|
4
|
+
data.tar.gz: 97b7efc3432c0b51b052bad9201e87a900791e57afdc9d2d9ef6444d654891bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 629f66d456631f91b36200dd03266c2de827dd6313ded03a49c01f0021b303f93b98f8c9f5525e831ba9412412ed908800fd831fc89282c16c155804ae8d0b79
|
7
|
+
data.tar.gz: 63c9fc8f12c8e25f4230e39298ab623b7b85e5b491679ae0a9c59f4bf6da9122cf1991a750df8376d81e482c8bb3063a7fce8f22b3812ee3fd97e83c3dcd97d7
|
data/.DS_Store
ADDED
Binary file
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
looks_good (1.0.0)
|
5
|
+
capybara (>= 2.6.0)
|
6
|
+
rmagick (>= 2.15.4)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
addressable (2.8.0)
|
12
|
+
public_suffix (>= 2.0.2, < 5.0)
|
13
|
+
capybara (3.35.3)
|
14
|
+
addressable
|
15
|
+
mini_mime (>= 0.1.3)
|
16
|
+
nokogiri (~> 1.8)
|
17
|
+
rack (>= 1.6.0)
|
18
|
+
rack-test (>= 0.6.3)
|
19
|
+
regexp_parser (>= 1.5, < 3.0)
|
20
|
+
xpath (~> 3.2)
|
21
|
+
childprocess (0.9.0)
|
22
|
+
ffi (~> 1.0, >= 1.0.11)
|
23
|
+
coderay (1.1.3)
|
24
|
+
ffi (1.15.5)
|
25
|
+
method_source (1.0.0)
|
26
|
+
mini_mime (1.1.2)
|
27
|
+
mini_portile2 (2.6.1)
|
28
|
+
nokogiri (1.12.5)
|
29
|
+
mini_portile2 (~> 2.6.1)
|
30
|
+
racc (~> 1.4)
|
31
|
+
pry (0.14.1)
|
32
|
+
coderay (~> 1.1)
|
33
|
+
method_source (~> 1.0)
|
34
|
+
public_suffix (4.0.7)
|
35
|
+
racc (1.6.0)
|
36
|
+
rack (2.2.4)
|
37
|
+
rack-test (2.0.2)
|
38
|
+
rack (>= 1.3)
|
39
|
+
rake (0.9.2)
|
40
|
+
regexp_parser (2.5.0)
|
41
|
+
rmagick (4.2.6)
|
42
|
+
rubyzip (1.3.0)
|
43
|
+
selenium-webdriver (2.53.4)
|
44
|
+
childprocess (~> 0.5)
|
45
|
+
rubyzip (~> 1.0)
|
46
|
+
websocket (~> 1.0)
|
47
|
+
websocket (1.2.9)
|
48
|
+
xpath (3.2.0)
|
49
|
+
nokogiri (~> 1.8)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
looks_good!
|
56
|
+
pry (>= 0.10.2)
|
57
|
+
rake (>= 0.9.2)
|
58
|
+
selenium-webdriver (~> 2.53.0)
|
59
|
+
|
60
|
+
BUNDLED WITH
|
61
|
+
2.2.5
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Gabriel Rotbart, Amanda Koh
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# LooksGood
|
2
|
+
|
3
|
+
A visual RSpec matcher to ensure the entire page (or specific elements) have not changed. Allows for a tolerance to be set, so you can reuire pixel-perfect or allow for some minor changes.
|
4
|
+
|
5
|
+
-------------------------------------
|
6
|
+
|
7
|
+
## Installation:
|
8
|
+
|
9
|
+
gem install looks_good
|
10
|
+
|
11
|
+
In rails_helper.rb:
|
12
|
+
|
13
|
+
require 'looks_good'
|
14
|
+
require 'looks_good/matchers/look_like_matcher'
|
15
|
+
|
16
|
+
add to your `.gitignore`
|
17
|
+
|
18
|
+
spec/screenshots/tmp/*
|
19
|
+
|
20
|
+
### Usage:
|
21
|
+
|
22
|
+
Identify an element to match, for example:
|
23
|
+
|
24
|
+
@element = page.find(:css, 'main')
|
25
|
+
|
26
|
+
Use LooksGood's custom matcher and specify the reference image:
|
27
|
+
|
28
|
+
expect(@element).to look_like(:main_section)
|
29
|
+
|
30
|
+
LooksGood will take care of cropping the element and try and make a targeted match without the noise of the whole page.
|
31
|
+
The parameter passed to `look_like` is the name that will be used to store the initial screenshot, and will be used in subsequent comparisons.
|
32
|
+
|
33
|
+
`look_like` can also handle the file naming and storage, if you specify a symbol. If the above example is from the spec `system/frontend/login_spec.rb`, then the screenshot will be stored in `spec/screenshots/system/frontend/login/main_section.png`.
|
34
|
+
|
35
|
+
You can ovveride this automatic behavior by specifying the file path as a string.
|
36
|
+
|
37
|
+
expect(@element).to look_like("frontend/main-screen.png")
|
38
|
+
|
39
|
+
which would store the screenshot in `spec/screenshots/frontend/main-screen.png`
|
40
|
+
|
41
|
+
If you want to adjust how tolerant the matcher is of differences, specify a float between 0 and 1 to `within`
|
42
|
+
|
43
|
+
expect(@element).to look_like(:main_content, within: 0.05)
|
44
|
+
|
45
|
+
If no reference image exits, the test will pass and a reference file will be created for future comparisons.
|
46
|
+
|
47
|
+
If you encounter a failure for a desired change, a diff image will be created to visualze the differences as well as a reference to the new version.
|
48
|
+
you can either copy the file provided in the error, or run your test suite again with `LOOKS_GOOD=true` to update it to the new version.
|
49
|
+
|
50
|
+
|
51
|
+
-------------------------------------
|
52
|
+
|
53
|
+
### Configuration settings:
|
54
|
+
|
55
|
+
you can override any of the defaults with a config block.
|
56
|
+
|
57
|
+
#### Configuration settings are set with the following:
|
58
|
+
|
59
|
+
LooksGood.config do |c|
|
60
|
+
c.reference_image_path = 'spec/screenshots'
|
61
|
+
c.max_no_tries = 1
|
62
|
+
c.default_within = 0.01
|
63
|
+
c.sleep_between_tries = 0.5
|
64
|
+
c.browser_folders = false
|
65
|
+
c.scale_amount = 0.5
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
#### reference_image_path - sets where the reference and diff images are saved to.
|
70
|
+
|
71
|
+
For Rails application, a default images folder will be created at spec/screenshots. This folder is root to all the reference
|
72
|
+
images to be compares, but can be changed.
|
73
|
+
|
74
|
+
Also created are subfolders:
|
75
|
+
- tmp/ - will hold temporary/transient images. add this to .gitignore.
|
76
|
+
- tmp/candidate/ - will hold candidate images which can be used as the new versions of reference images
|
77
|
+
- tmp/diff/ - will hold the visual diff images for inspection when comparison fails
|
78
|
+
|
79
|
+
#### max_no_tries
|
80
|
+
- sets how many times looks_good will try and match the element against the reference image. Handy to reduce fragility of tests due to animations and load times. Defaults to 1.
|
81
|
+
|
82
|
+
#### default_within
|
83
|
+
- a float between 0 and 1 that sets the default tolerance for visual differences. default is 0.01 (1%)
|
84
|
+
|
85
|
+
#### scale_amount
|
86
|
+
- Retina mac screenshots are 2x actual size, so this scales them to be 1:1. default is 0.5, set to 1 to disable.
|
87
|
+
|
88
|
+
#### Sleep_between_tries
|
89
|
+
- sets the sleep time (in seconds) between match tries (requires max_no_tries > 1). Defaults to 0.5
|
90
|
+
|
91
|
+
#### browser_folders
|
92
|
+
- *Currently only available with Selenium-Webdriver / Capybara* - create reference folders based on the current Selenium driver's browser. Allows for cross-browser visual testing.
|
93
|
+
|
94
|
+
|
95
|
+
-------------------------------------
|
96
|
+
|
97
|
+
#### Non-gem dependencies:
|
98
|
+
|
99
|
+
Imagemagick must be installed:
|
100
|
+
|
101
|
+
$sudo apt-get install imagemagick
|
102
|
+
|
103
|
+
-------------------------------------
|
104
|
+
|
105
|
+
|
106
|
+
## Contributing to `looks_good`
|
107
|
+
|
108
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
109
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
110
|
+
* Fork the project
|
111
|
+
* Start a feature/bugfix branch
|
112
|
+
* Commit and push until you are happy with your contribution
|
113
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
114
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
115
|
+
|
116
|
+
== History
|
117
|
+
looks_good is an expansion of (gatling)[https://github.com/gabrielrotbart/gatling] with added functionality and updated dependecies.
|
118
|
+
|
119
|
+
== Copyright
|
120
|
+
|
121
|
+
Copyright (c) 2022 Russell Jennings See LICENSE.txt for
|
122
|
+
further details.
|
123
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'looks_good'
|
3
|
+
|
4
|
+
module LooksGood
|
5
|
+
module CaptureElement
|
6
|
+
extend LooksGood::Configuration
|
7
|
+
|
8
|
+
def self.capture(element)
|
9
|
+
# Getting the element position before screenshot because of a side effect
|
10
|
+
# of WebDrivers getLocationOnceScrolledIntoView method which scrolls the page
|
11
|
+
# regardless of whether the object is in view or not
|
12
|
+
element_position = get_element_position(element)
|
13
|
+
screenshot = take_screenshot
|
14
|
+
|
15
|
+
crop_element(screenshot, element, element_position)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.take_screenshot
|
19
|
+
temp_dir = LooksGood::Configuration.path(:temp)
|
20
|
+
FileUtils.mkdir_p(temp_dir) unless File.exists?(temp_dir)
|
21
|
+
#captures the uncropped full screen
|
22
|
+
begin
|
23
|
+
temp_screenshot_filename = File.join(temp_dir, "temp-#{Process.pid}.png")
|
24
|
+
Capybara.page.driver.browser.save_screenshot(temp_screenshot_filename)
|
25
|
+
temp_screenshot = Magick::Image.read(temp_screenshot_filename).first
|
26
|
+
rescue
|
27
|
+
raise "Could not save screenshot to #{temp_dir}. Please make sure you have permission"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get_element_position(element)
|
32
|
+
element = element.native
|
33
|
+
position = Hash.new{}
|
34
|
+
position[:x] = element.location.x
|
35
|
+
position[:y] = element.location.y
|
36
|
+
position[:width] = element.size.width
|
37
|
+
position[:height] = element.size.height
|
38
|
+
position
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.crop_element(image, element_to_crop, position)
|
42
|
+
cropped_element = image.scale(LooksGood::Configuration.scale_amount).crop(position[:x], position[:y], position[:width], position[:height])
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module LooksGood
|
2
|
+
class Comparison
|
3
|
+
|
4
|
+
attr_accessor :diff_image, :actual_image, :expected_image
|
5
|
+
|
6
|
+
def initialize(actual_image, expected_image, within=LooksGood::Configuration.default_within)
|
7
|
+
@actual_image = actual_image
|
8
|
+
@expected_image = expected_image
|
9
|
+
@comparison = compare_image
|
10
|
+
@within = within
|
11
|
+
# within is a float of % image difference between the two images
|
12
|
+
@match = @comparison[1] <= @within
|
13
|
+
@diff_image =LooksGood::Image.new(@comparison.first, @expected_image.file_name) unless @matches
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?
|
17
|
+
@match
|
18
|
+
end
|
19
|
+
|
20
|
+
def within
|
21
|
+
@within
|
22
|
+
end
|
23
|
+
|
24
|
+
def percent_difference
|
25
|
+
@comparison[1]
|
26
|
+
end
|
27
|
+
|
28
|
+
def compare_image
|
29
|
+
compare_images_with_same_size? ? compare_images_with_same_size : compare_images_with_different_size
|
30
|
+
end
|
31
|
+
|
32
|
+
def compare_images_with_same_size
|
33
|
+
images_to_compare = prep_images_for_comparison
|
34
|
+
images_to_compare.first.compare_channel(images_to_compare.last, Magick::PeakAbsoluteErrorMetric, Magick::AllChannels) do
|
35
|
+
self.highlight_color = Magick::Pixel.new(65300,100,0,38000)
|
36
|
+
self.lowlight_color = Magick::Pixel.new(0,65300,1000,60000)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def compare_images_with_different_size
|
41
|
+
row = [@actual_image.image.rows, @expected_image.image.rows].max
|
42
|
+
column = [@actual_image.image.columns, @expected_image.image.columns].max
|
43
|
+
|
44
|
+
images_to_compare = prep_images_for_comparison do |image|
|
45
|
+
expanded_image = image.extent(column, row)
|
46
|
+
expanded_image.background_color = 'white'
|
47
|
+
expanded_image
|
48
|
+
end
|
49
|
+
images_to_compare.first.compare_channel(images_to_compare.last, Magick::PeakAbsoluteErrorMetric) do |img|
|
50
|
+
img.highlight_color = Magick::Pixel.new(65300,100,0,38000)
|
51
|
+
img.lowlight_color = Magick::Pixel.new(0,65300,1000,60000)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def compare_images_with_same_size?
|
56
|
+
@actual_image.image.rows == @expected_image.image.rows && @actual_image.image.columns == @expected_image.image.columns
|
57
|
+
end
|
58
|
+
|
59
|
+
def prep_images_for_comparison
|
60
|
+
[
|
61
|
+
@actual_image,
|
62
|
+
@expected_image,
|
63
|
+
].collect do |looks_good_image|
|
64
|
+
image = looks_good_image.image.clone
|
65
|
+
image = yield image if block_given?
|
66
|
+
|
67
|
+
# Important: ensure the image 0,0 is reset to the top-left of the image before comparison
|
68
|
+
image.offset = 0
|
69
|
+
image
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module LooksGood
|
4
|
+
module Configuration
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_accessor :reference_image_path, :max_no_tries, :sleep_between_tries, :browser_folders, :default_within
|
9
|
+
|
10
|
+
attr_reader :paths
|
11
|
+
|
12
|
+
def reference_image_path
|
13
|
+
@reference_image_path ||= default_reference_path
|
14
|
+
@browser_folders ? (reference_path_with_browser_folders) : @reference_image_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_within
|
18
|
+
@default_within ||= 0.01 # 1%
|
19
|
+
end
|
20
|
+
|
21
|
+
# allows retina mac screenshots to be scaled to expected size
|
22
|
+
def scale_amount
|
23
|
+
@scale_amount ||= 0.5
|
24
|
+
end
|
25
|
+
|
26
|
+
def max_no_tries
|
27
|
+
@max_no_tries ||= 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def sleep_between_tries
|
31
|
+
@sleep_between_tries ||= 0.1
|
32
|
+
end
|
33
|
+
|
34
|
+
def path(type)
|
35
|
+
paths = {:reference => reference_image_path,
|
36
|
+
:temp => File.join(reference_image_path, 'tmp', 'tmp'),
|
37
|
+
:candidate => File.join(reference_image_path, 'temp', 'candidate'),
|
38
|
+
:diff => File.join(reference_image_path, 'temp', 'diff')
|
39
|
+
}
|
40
|
+
paths[type]
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_reference_path
|
44
|
+
begin
|
45
|
+
reference_image_path = File.join(Rails.root, 'spec/screenshots')
|
46
|
+
rescue
|
47
|
+
reference_image_path = 'spec/screenshots'
|
48
|
+
puts "Currently defaulting to #{@reference_image_path}. Overide this by setting reference_image_path=[refpath] in your configuration block"
|
49
|
+
end
|
50
|
+
reference_image_path
|
51
|
+
end
|
52
|
+
|
53
|
+
def reference_path_with_browser_folders
|
54
|
+
begin
|
55
|
+
reference_images_path = File.join(@reference_image_path, browser)
|
56
|
+
rescue
|
57
|
+
reference_images_path = @reference_image_path
|
58
|
+
end
|
59
|
+
reference_images_path
|
60
|
+
end
|
61
|
+
|
62
|
+
def browser
|
63
|
+
begin
|
64
|
+
browser = Capybara.page.driver.browser.browser
|
65
|
+
rescue
|
66
|
+
browser = Selenium.page.driver.browser.browser
|
67
|
+
rescue
|
68
|
+
raise "Currently custom folders are only supported by Capybara"
|
69
|
+
return nil
|
70
|
+
end
|
71
|
+
browser.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module LooksGood
|
2
|
+
class Image
|
3
|
+
|
4
|
+
attr_accessor :file_name, :path, :image
|
5
|
+
|
6
|
+
attr_reader :type
|
7
|
+
|
8
|
+
def initialize image, file_name
|
9
|
+
@file_name = file_name
|
10
|
+
@image = image
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(type = :reference)
|
14
|
+
save_path = path(type)
|
15
|
+
FileUtils::mkdir_p(File.dirname(save_path)) unless File.exists?(save_path)
|
16
|
+
@image.write save_path
|
17
|
+
save_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def exists?
|
21
|
+
File.exists?(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def path(type = :reference)
|
25
|
+
@path = File.join(LooksGood::Configuration.path(type), @file_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class ImageFromElement < Image
|
31
|
+
|
32
|
+
def initialize(element, file_name)
|
33
|
+
super(image, file_name)
|
34
|
+
@element = element
|
35
|
+
@image = capture_image
|
36
|
+
end
|
37
|
+
|
38
|
+
def verify_and_save
|
39
|
+
LooksGood::Configuration.max_no_tries.times do
|
40
|
+
comparable = capture_image
|
41
|
+
matches = LooksGood::Comparison.new(self,Image.new(comparable,@file_name)).matches?
|
42
|
+
if matches
|
43
|
+
self.save
|
44
|
+
puts "Saved #{self.path} as reference"
|
45
|
+
return()
|
46
|
+
else
|
47
|
+
@image = comparable
|
48
|
+
end
|
49
|
+
end
|
50
|
+
raise 'Could not save a stable image. This could be due to animations or page load times. Saved a reference image, delete it to re-try'
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def capture_image
|
55
|
+
LooksGood::CaptureElement.capture(@element)
|
56
|
+
end
|
57
|
+
|
58
|
+
#TODO: make save a relevant subclass method
|
59
|
+
end
|
60
|
+
|
61
|
+
class ImageFromFile < Image
|
62
|
+
|
63
|
+
def initialize(file_name)
|
64
|
+
super(image, file_name)
|
65
|
+
|
66
|
+
@image = Magick::Image.read(path).first
|
67
|
+
end
|
68
|
+
|
69
|
+
#TODO: make save a relevant subclass method
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'looks_good'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :look_like do |expected|
|
4
|
+
result = nil
|
5
|
+
match do |actual|
|
6
|
+
if expected.is_a?(Symbol)
|
7
|
+
called_by_file = self.caller.find{|str| str.include?("_spec.rb")}
|
8
|
+
path_to = called_by_file.split("_spec.rb").first.split("spec/").last
|
9
|
+
expected = File.join(path_to, "#{expected}.png")
|
10
|
+
end
|
11
|
+
result = LooksGood.check(expected, actual)
|
12
|
+
result[:result]
|
13
|
+
end
|
14
|
+
|
15
|
+
failure_message do |actual|
|
16
|
+
actual_amount = result[:percent_difference] * 100
|
17
|
+
expected_amount = result[:comparison].within * 100
|
18
|
+
error_message = "expected '#{self.actual.path}' to match previous snapshot #{expected} by #{expected_amount.round(3)}%, but was off by #{actual_amount.round(3)}%\n"
|
19
|
+
error_message += result[:message]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
data/lib/looks_good.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'rmagick'
|
2
|
+
require 'capybara'
|
3
|
+
require 'capybara/dsl'
|
4
|
+
|
5
|
+
require 'looks_good/configuration'
|
6
|
+
require 'looks_good/image'
|
7
|
+
require 'looks_good/comparison'
|
8
|
+
require 'looks_good/capture_element'
|
9
|
+
require 'looks_good/rspec_config'
|
10
|
+
|
11
|
+
module LooksGood
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def check(expected_reference_filename, actual_element, within: LooksGood::Configuration.default_within)
|
15
|
+
result = match_result(expected_reference_filename, actual_element, within: within)
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def match_result(expected_reference_filename, actual_element, within:)
|
20
|
+
result_hash = {}
|
21
|
+
@actual_element = actual_element
|
22
|
+
@expected_reference_filename = expected_reference_filename
|
23
|
+
@expected_reference_file = (File.join(LooksGood::Configuration.path(:reference), expected_reference_filename))
|
24
|
+
|
25
|
+
|
26
|
+
if !File.exists?(@expected_reference_file) || ENV["LOOKS_GOOD"]
|
27
|
+
save_reference
|
28
|
+
result_hash[:result] = true
|
29
|
+
return result_hash
|
30
|
+
else
|
31
|
+
reference_file = LooksGood::ImageFromFile.new(expected_reference_filename)
|
32
|
+
comparison = compare_until_match(actual_element, reference_file, within: within)
|
33
|
+
result_hash[:comparison] = comparison
|
34
|
+
result_hash[:percent_difference] = comparison.percent_difference
|
35
|
+
matches = comparison.matches?
|
36
|
+
if !matches
|
37
|
+
comparison.actual_image.save(:candidate)
|
38
|
+
save_image_as_diff(comparison.diff_image)
|
39
|
+
result_hash[:message] = %Q[view a visual diff image: open #{comparison.diff_image.path(:diff)}\n
|
40
|
+
HOW TO FIX:\n
|
41
|
+
- cp #{comparison.diff_image.path(:candidate)} #{@expected_reference_file}
|
42
|
+
or
|
43
|
+
- LOOKS_GOOD=true rspec ...]
|
44
|
+
result_hash[:result] = false
|
45
|
+
result_hash
|
46
|
+
else
|
47
|
+
result_hash[:result] = true
|
48
|
+
result_hash
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def compare_until_match(actual_element, reference_file, within:, max_no_tries: LooksGood::Configuration.max_no_tries, sleep_time: LooksGood::Configuration.sleep_between_tries)
|
54
|
+
max_no_tries.times do |i|
|
55
|
+
actual_image = LooksGood::ImageFromElement.new(actual_element, reference_file.file_name)
|
56
|
+
@comparison = LooksGood::Comparison.new(actual_image, reference_file, within)
|
57
|
+
match = @comparison.matches?
|
58
|
+
if !match
|
59
|
+
sleep sleep_time
|
60
|
+
#TODO: Send to logger instead of puts
|
61
|
+
i += 1
|
62
|
+
puts "Tried to match #{i} times"
|
63
|
+
else
|
64
|
+
return(@comparison)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@comparison
|
68
|
+
end
|
69
|
+
|
70
|
+
def save_image_as_diff(image)
|
71
|
+
image.save(:diff)
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
def save_image_as_candidate(image)
|
76
|
+
image.save(:candidate)
|
77
|
+
raise "The design reference #{image.file_name} does not exist, #{image.path(:candidate)} " +
|
78
|
+
"is now available to be used as a reference. Copy candidate to root reference_image_path to use as reference"
|
79
|
+
end
|
80
|
+
|
81
|
+
def save_reference
|
82
|
+
ImageFromElement.new(@actual_element,@expected_reference_filename).verify_and_save
|
83
|
+
end
|
84
|
+
|
85
|
+
def cleanup
|
86
|
+
FileUtils.remove_dir(LooksGood::Configuration.path(:temp)) if File.directory?(LooksGood::Configuration.path(:temp))
|
87
|
+
FileUtils.remove_dir(LooksGood::Configuration.path(:diff)) if File.directory?(LooksGood::Configuration.path(:diff))
|
88
|
+
FileUtils.remove_dir(LooksGood::Configuration.path(:candidate)) if File.directory?(LooksGood::Configuration.path(:candidate))
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def config(&block)
|
93
|
+
begin
|
94
|
+
config_class = LooksGood::Configuration
|
95
|
+
raise "No block provied" unless block_given?
|
96
|
+
block.call(config_class)
|
97
|
+
rescue
|
98
|
+
raise "Config block has changed. Example: LooksGood.config {|c| c.reference_image_path = 'some/path'}. Please see README"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
data/looks_good.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "looks_good/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "looks_good"
|
7
|
+
s.version = LooksGood::VERSION
|
8
|
+
s.authors = ["Russell Jennings"]
|
9
|
+
s.email = ["violentpurr@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/meesterdude/looks_good"
|
11
|
+
s.summary = %q{Rspec visual testing}
|
12
|
+
s.description = %q{Rspec visual testing with percent matching tolerance}
|
13
|
+
|
14
|
+
s.rubyforge_project = "looks_good"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency('rmagick', ['>=2.15.4'])
|
22
|
+
s.add_runtime_dependency('capybara',['>=2.6.0'])
|
23
|
+
|
24
|
+
s.add_development_dependency('rake',['>=0.9.2'])
|
25
|
+
s.add_development_dependency('pry',['>=0.10.2'])
|
26
|
+
s.add_development_dependency('selenium-webdriver',['~> 2.53.0'])
|
27
|
+
end
|