lineup 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +62 -0
- data/README.md +143 -0
- data/bin/lineup +7 -0
- data/doc/example.png +0 -0
- data/lib/controller/browser.rb +58 -0
- data/lib/controller/comparer.rb +79 -0
- data/lib/helper.rb +31 -0
- data/lib/lineup.rb +275 -0
- data/lib/lineup/version.rb +13 -0
- data/lineup.gemspec +16 -0
- data/tests/rspec/lineup_spec.rb +217 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: ca168f6253bc6111e22696d8b4f72e5a468ca804
|
|
4
|
+
data.tar.gz: bc2ffad3a74d03b3dbff81e5f980d81fd1c76644
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a87243428920002cd40dc99f824d97dc98630dccd022cc579e3dea71718598b7242fab291abf0b85dac13a86596b9dd0b394c1644ce48d32bda1b73badcd4592
|
|
7
|
+
data.tar.gz: 10ea27a5f54e0cb04ed9da5881ba276978d599c29be3c38dfb7fb983fd11349365746ac7cf93e70aaa630a48979da66688f95c355a2e4359b82cc6cf4e92935f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
lineup (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
childprocess (0.5.5)
|
|
10
|
+
ffi (~> 1.0, >= 1.0.11)
|
|
11
|
+
chunky_png (1.3.4)
|
|
12
|
+
commonwatir (4.0.0)
|
|
13
|
+
diff-lcs (1.2.5)
|
|
14
|
+
dimensions (1.3.0)
|
|
15
|
+
ffi (1.9.6)
|
|
16
|
+
headless (2.0.0)
|
|
17
|
+
multi_json (1.11.0)
|
|
18
|
+
oily_png (1.2.0)
|
|
19
|
+
chunky_png (~> 1.3.1)
|
|
20
|
+
pxdoppelganger (0.1.1)
|
|
21
|
+
rspec (3.2.0)
|
|
22
|
+
rspec-core (~> 3.2.0)
|
|
23
|
+
rspec-expectations (~> 3.2.0)
|
|
24
|
+
rspec-mocks (~> 3.2.0)
|
|
25
|
+
rspec-core (3.2.3)
|
|
26
|
+
rspec-support (~> 3.2.0)
|
|
27
|
+
rspec-expectations (3.2.1)
|
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
29
|
+
rspec-support (~> 3.2.0)
|
|
30
|
+
rspec-mocks (3.2.1)
|
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
32
|
+
rspec-support (~> 3.2.0)
|
|
33
|
+
rspec-support (3.2.2)
|
|
34
|
+
rubyzip (1.1.7)
|
|
35
|
+
selenium-webdriver (2.46.2)
|
|
36
|
+
childprocess (~> 0.5)
|
|
37
|
+
multi_json (~> 1.0)
|
|
38
|
+
rubyzip (~> 1.0)
|
|
39
|
+
websocket (~> 1.0)
|
|
40
|
+
watir (5.0.0)
|
|
41
|
+
commonwatir (~> 4)
|
|
42
|
+
watir-webdriver
|
|
43
|
+
watir-webdriver (0.7.0)
|
|
44
|
+
selenium-webdriver (>= 2.45)
|
|
45
|
+
websocket (1.2.1)
|
|
46
|
+
|
|
47
|
+
PLATFORMS
|
|
48
|
+
ruby
|
|
49
|
+
|
|
50
|
+
DEPENDENCIES
|
|
51
|
+
dimensions
|
|
52
|
+
headless
|
|
53
|
+
lineup!
|
|
54
|
+
oily_png
|
|
55
|
+
pxdoppelganger (= 0.1.1)
|
|
56
|
+
rspec
|
|
57
|
+
selenium-webdriver
|
|
58
|
+
watir
|
|
59
|
+
watir-webdriver
|
|
60
|
+
|
|
61
|
+
BUNDLED WITH
|
|
62
|
+
1.10.6
|
data/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Lineup
|
|
2
|
+
|
|
3
|
+
Lineup is doing automated testing of webpage designs, eg. in continious delivery.
|
|
4
|
+
If you push new code to production, you can evaluate the design of you page compared to a defined base design and
|
|
5
|
+
get an analysis about the difference of the designs:
|
|
6
|
+
|
|
7
|
+
For all images that you want to compare, you will receive information about how many pixel are different
|
|
8
|
+
between the two image version and an image, that contains only the parts, that changed, a "difference image".
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
Picture: Example view of base (left), new (right) as well as diff image. In this example, the margin around the bottom headline increased,
|
|
12
|
+
thus some of the elements moved down.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
A firefox browser must be installed, as well as phantomjs.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Add it into your Gemfile:
|
|
21
|
+
````ruby
|
|
22
|
+
gem "lineup"
|
|
23
|
+
````
|
|
24
|
+
|
|
25
|
+
Or install it manually with the following command:
|
|
26
|
+
````
|
|
27
|
+
gem install lineup
|
|
28
|
+
````
|
|
29
|
+
|
|
30
|
+
Do a base reference screenshot of you application:
|
|
31
|
+
````ruby
|
|
32
|
+
require 'lineup'
|
|
33
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
34
|
+
lineup.record_screenshot('base')
|
|
35
|
+
````
|
|
36
|
+
|
|
37
|
+
Do something (deployment of your new code) and take a new screenshot
|
|
38
|
+
````ruby
|
|
39
|
+
lineup.record_screenshot('new')
|
|
40
|
+
````
|
|
41
|
+
|
|
42
|
+
Analyse the results:
|
|
43
|
+
````ruby
|
|
44
|
+
lineup.compare('new', 'base')
|
|
45
|
+
=> [{:url => 'sport', :width => 600, :difference => 0.7340442722738748,
|
|
46
|
+
:base_file => '/home/name/lineup/screenshots/base_frontpage_600.png'
|
|
47
|
+
:new_file => '/home/name/lineup/screenshots/new_frontpage_600.png'
|
|
48
|
+
:diff_file => '/home/name/lineup/screenshots/DIFFERENCE_frontpage_600.png' }]
|
|
49
|
+
````
|
|
50
|
+
|
|
51
|
+
You can save it for later use:
|
|
52
|
+
````ruby
|
|
53
|
+
lineup.save_json('/home/name/lineup/results/')
|
|
54
|
+
=> '/home/name/lineup/results/json.log'
|
|
55
|
+
````
|
|
56
|
+
|
|
57
|
+
## More configuration:
|
|
58
|
+
|
|
59
|
+
There are multiple ways to specify what to lineup and compare.
|
|
60
|
+
|
|
61
|
+
By specifying different urls via ````#urls````:
|
|
62
|
+
````ruby
|
|
63
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
64
|
+
lineup.urls("/, /multimedia, /sport")
|
|
65
|
+
````
|
|
66
|
+
This will do analysis of otto.de root (frontpage), otto.de/multimedia and otto.de/sport.
|
|
67
|
+
It requires a comma separated string. Default value is only root.
|
|
68
|
+
|
|
69
|
+
By specifying different resolutions via ````#resolutions````:
|
|
70
|
+
````ruby
|
|
71
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
72
|
+
lineup.resolutions("600, 800, 1200")
|
|
73
|
+
````
|
|
74
|
+
The values are the browser width in pixel. For each size an analysis is done.
|
|
75
|
+
It require a comma separated string. Default values are 640px, 800px and 1180px.
|
|
76
|
+
|
|
77
|
+
By specifying a filepath for the screenshots via ````#filepath_for_images````:
|
|
78
|
+
````ruby
|
|
79
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
80
|
+
lineup.filepath_for_images(/home/myname/lineup/screenshots)
|
|
81
|
+
````
|
|
82
|
+
Creates a file and saves the screenshots in the file. Default is ````"#{Dir.pwd}/screenshots"````
|
|
83
|
+
|
|
84
|
+
By specifying a filepath for the difference image via ````#difference_path````:
|
|
85
|
+
````ruby
|
|
86
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
87
|
+
lineup.difference_path(/home/myname/lineup/result)
|
|
88
|
+
````
|
|
89
|
+
Creates a file and saves the difference image in the file. Default is ````"#{Dir.pwd}/screenshots"````
|
|
90
|
+
|
|
91
|
+
By specifying wether or not to use phantomjs via ````#use_phantomjs````:
|
|
92
|
+
````ruby
|
|
93
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
94
|
+
lineup.use_phantomjs(true)
|
|
95
|
+
````
|
|
96
|
+
If ````false```` the screenshots are taken in Firefox. ````#load_json_config````:
|
|
97
|
+
|
|
98
|
+
Load all above configs from a json file via
|
|
99
|
+
````ruby
|
|
100
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
101
|
+
lineup.load_json_config(/home/myname/lineup/config.json)
|
|
102
|
+
````
|
|
103
|
+
While my file contains all relevant information
|
|
104
|
+
````json
|
|
105
|
+
{
|
|
106
|
+
"urls":"/multimedia, /sport",
|
|
107
|
+
"resolutions":"600,800,1200",
|
|
108
|
+
"filepath_for_images":"~/images/",
|
|
109
|
+
"use_headless":true,
|
|
110
|
+
"difference_path":"#/images/diff"
|
|
111
|
+
}
|
|
112
|
+
````
|
|
113
|
+
|
|
114
|
+
## Example:
|
|
115
|
+
|
|
116
|
+
````ruby
|
|
117
|
+
base_name = 'name-for-base-screenshot'
|
|
118
|
+
new_name = 'name-for-new-screenshot'
|
|
119
|
+
urls = '/, multimedia, sport'
|
|
120
|
+
resolutions = '600, 800, 1200'
|
|
121
|
+
images_path = '/home/myname/lineup/screenshots'
|
|
122
|
+
difference_path = '/home/myname/lineup/results'
|
|
123
|
+
json_path = 'home/myname/lineup/results'
|
|
124
|
+
phantom = true
|
|
125
|
+
|
|
126
|
+
lineup = Lineup::Screenshot.new('https://www.otto.de')
|
|
127
|
+
lineup.urls(urls)
|
|
128
|
+
lineup.resolutions(resolutions)
|
|
129
|
+
lineup.filepath_for_images(images_path
|
|
130
|
+
lineup.difference_path(difference_path)
|
|
131
|
+
|
|
132
|
+
lineup.record_screenshot(base_name)
|
|
133
|
+
# do sth. (eg. deploy new software)
|
|
134
|
+
lineup.record_screenshot(new_name)
|
|
135
|
+
lineup.save_json(json_path)
|
|
136
|
+
````
|
|
137
|
+
Now open home/myname/lineup/results and find:
|
|
138
|
+
the difference files and a log.json with all information about what images are not the same.
|
|
139
|
+
|
|
140
|
+
## Contribute
|
|
141
|
+
|
|
142
|
+
Please do!
|
|
143
|
+
|
data/bin/lineup
ADDED
data/doc/example.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'watir'
|
|
2
|
+
require 'watir-webdriver'
|
|
3
|
+
include Selenium
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'headless'
|
|
6
|
+
require_relative '../helper'
|
|
7
|
+
|
|
8
|
+
class Browser
|
|
9
|
+
|
|
10
|
+
def initialize(baseurl, urls, resolutions, path, headless)
|
|
11
|
+
@absolute_image_path = path
|
|
12
|
+
FileUtils.mkdir_p @absolute_image_path
|
|
13
|
+
@baseurl = baseurl
|
|
14
|
+
@urls = urls
|
|
15
|
+
@resolutions = resolutions
|
|
16
|
+
@headless = headless
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record(version)
|
|
20
|
+
browser_loader
|
|
21
|
+
@urls.each do |url|
|
|
22
|
+
@resolutions.each do |width|
|
|
23
|
+
screenshot_recorder(width, url, version)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def end
|
|
29
|
+
begin #Timeout::Error
|
|
30
|
+
Timeout::timeout(10) { @browser.close }
|
|
31
|
+
rescue Timeout::Error
|
|
32
|
+
browser_pid = @browser.driver.instance_variable_get(:@bridge).instance_variable_get(:@service).instance_variable_get(:@process).pid
|
|
33
|
+
::Process.kill('KILL', browser_pid)
|
|
34
|
+
sleep 1
|
|
35
|
+
end
|
|
36
|
+
sleep 5 # to prevent xvfb to freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def browser_loader
|
|
42
|
+
if @headless
|
|
43
|
+
@browser = Watir::Browser.new :phantomjs
|
|
44
|
+
else
|
|
45
|
+
@browser = Watir::Browser.new :firefox
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def screenshot_recorder(width, url, version)
|
|
50
|
+
filename = Helper.filename(@absolute_image_path, url, width, version)
|
|
51
|
+
@browser.driver.manage.window.resize_to(width, 1000)
|
|
52
|
+
@browser.cookies.clear
|
|
53
|
+
url = Helper.url(@baseurl, url)
|
|
54
|
+
@browser.goto url
|
|
55
|
+
@browser.screenshot.save( File.expand_path(filename))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'pxdoppelganger'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
class Comparer
|
|
5
|
+
|
|
6
|
+
attr_accessor :difference
|
|
7
|
+
|
|
8
|
+
def initialize(base, new, difference_path, baseurl, urls, resolutions, screenshot_path)
|
|
9
|
+
@base = base
|
|
10
|
+
@new = new
|
|
11
|
+
@baseurl = baseurl
|
|
12
|
+
@urls = urls
|
|
13
|
+
@resolutions = resolutions
|
|
14
|
+
@absolute_image_path = screenshot_path
|
|
15
|
+
@difference_path = difference_path
|
|
16
|
+
FileUtils.mkdir_p difference_path
|
|
17
|
+
compare_images
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def json(path)
|
|
21
|
+
FileUtils.mkdir_p path
|
|
22
|
+
generate_json(path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def compare_images
|
|
28
|
+
self.difference = []
|
|
29
|
+
@urls.each do |url|
|
|
30
|
+
@resolutions.each do |width|
|
|
31
|
+
base_name = Helper.filename(
|
|
32
|
+
@absolute_image_path,
|
|
33
|
+
url,
|
|
34
|
+
width,
|
|
35
|
+
@base
|
|
36
|
+
)
|
|
37
|
+
new_name = Helper.filename(
|
|
38
|
+
@absolute_image_path,
|
|
39
|
+
url,
|
|
40
|
+
width,
|
|
41
|
+
@new
|
|
42
|
+
)
|
|
43
|
+
images = PXDoppelganger::Images.new(
|
|
44
|
+
base_name,
|
|
45
|
+
new_name
|
|
46
|
+
)
|
|
47
|
+
if images.difference > 1e-03 # for changes bigger than 1 per 1.000; otherwise we see mathematical artifacts
|
|
48
|
+
diff_name = Helper.filename(
|
|
49
|
+
@difference_path,
|
|
50
|
+
url,
|
|
51
|
+
width,
|
|
52
|
+
'DIFFERENCE'
|
|
53
|
+
)
|
|
54
|
+
images.save_difference_image diff_name
|
|
55
|
+
result = {
|
|
56
|
+
url: url,
|
|
57
|
+
width: width,
|
|
58
|
+
difference: images.difference,
|
|
59
|
+
base_file: base_name,
|
|
60
|
+
new_file: new_name,
|
|
61
|
+
difference_file: diff_name
|
|
62
|
+
}
|
|
63
|
+
self.difference << result
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def generate_json(path)
|
|
70
|
+
json = difference.to_json
|
|
71
|
+
file = File.open(
|
|
72
|
+
"#{path}/log.json", "a+"
|
|
73
|
+
)
|
|
74
|
+
file.write(json)
|
|
75
|
+
file.close
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
end
|
data/lib/helper.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Helper
|
|
2
|
+
extend self
|
|
3
|
+
|
|
4
|
+
def filename(path, url, width, version)
|
|
5
|
+
"#{path}/#{version}_#{name(url)}_#{width}.png"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def url(base, url)
|
|
9
|
+
("#{base}/#{clean(url)}")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def name(page)
|
|
15
|
+
if page == '/'
|
|
16
|
+
name = 'frontpage'
|
|
17
|
+
else #remove forward slash
|
|
18
|
+
name = page.gsub(/\//, "")
|
|
19
|
+
end
|
|
20
|
+
name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clean(url)
|
|
24
|
+
if url == '/' #avoid two dashes at the end, e.g. www.otto.de//
|
|
25
|
+
''
|
|
26
|
+
else
|
|
27
|
+
url
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
data/lib/lineup.rb
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require_relative 'controller/browser'
|
|
3
|
+
require_relative 'controller/comparer'
|
|
4
|
+
|
|
5
|
+
module Lineup
|
|
6
|
+
|
|
7
|
+
attr_accessor :difference
|
|
8
|
+
|
|
9
|
+
class Screenshot
|
|
10
|
+
|
|
11
|
+
def initialize(baseurl)
|
|
12
|
+
|
|
13
|
+
# the base URL is the root url (in normal projects the frontpage or for us storefront)
|
|
14
|
+
# while the base url is passed in, defaults for other values are set, too
|
|
15
|
+
#
|
|
16
|
+
# for us the base url is https://www.otto.
|
|
17
|
+
#
|
|
18
|
+
# the base url needs to be a string and cannot be an empty sting
|
|
19
|
+
|
|
20
|
+
raise "base URL needs to be a string" unless baseurl.is_a? String
|
|
21
|
+
raise "base URL is needed, cannot be empty" if baseurl == ''
|
|
22
|
+
|
|
23
|
+
@baseurl = baseurl
|
|
24
|
+
|
|
25
|
+
# the urls are combined with the root url and give the absolute url of the pages to be tested
|
|
26
|
+
# see more in the according method below
|
|
27
|
+
# the default value is the baseurl itself, represented by a forward slash
|
|
28
|
+
# the images will be saved as "frontpage" images
|
|
29
|
+
|
|
30
|
+
urls('/')
|
|
31
|
+
|
|
32
|
+
# the resolutions can be as many as desired, we use a mobile, a tablet and a desktop resolution
|
|
33
|
+
# by default this is 640px, 800px and 1180px width
|
|
34
|
+
# see more in the according method below
|
|
35
|
+
|
|
36
|
+
resolutions('640, 800, 1180')
|
|
37
|
+
|
|
38
|
+
# this sets the path where to store the screenshots, by default this is the current directory
|
|
39
|
+
# see more in the according method below
|
|
40
|
+
|
|
41
|
+
filepath_for_images("#{Dir.pwd}/screenshots")
|
|
42
|
+
|
|
43
|
+
# using selenium in a headless environment vs firefox.
|
|
44
|
+
# by default in headless
|
|
45
|
+
# see more in according method below
|
|
46
|
+
|
|
47
|
+
use_phantomjs(true)
|
|
48
|
+
|
|
49
|
+
# this is the path where to save the difference images of two not alike screenshots
|
|
50
|
+
# by default the current directory, like for the other images
|
|
51
|
+
# see more in according method below
|
|
52
|
+
|
|
53
|
+
difference_path("#{Dir.pwd}/screenshots")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def urls(urls)
|
|
58
|
+
|
|
59
|
+
# all urls to be tested are defined here
|
|
60
|
+
# they need to be passed as a comma separated string (with or without whitespaces)
|
|
61
|
+
#
|
|
62
|
+
# e.g "/, /multimedia, /sport"
|
|
63
|
+
#
|
|
64
|
+
# the pages are used to name the image files, too
|
|
65
|
+
#
|
|
66
|
+
# if it is not a string or the string is empty an exception is raised
|
|
67
|
+
|
|
68
|
+
raise "url for screenshots needs to be a string" unless urls.is_a? String
|
|
69
|
+
raise "url for screenshots cannot be <empty string>" if urls == ''
|
|
70
|
+
|
|
71
|
+
# after the base screenshots are taken, the urls cannot be changed, an exception would be raised
|
|
72
|
+
|
|
73
|
+
raise_base_screenshots_taken('The urls')
|
|
74
|
+
|
|
75
|
+
#we remove whitespaces from the urls, replace ; by , and generate an array, splitted by comma
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
@urls= clean(urls).split(",")
|
|
79
|
+
rescue NoMethodError
|
|
80
|
+
raise "urls must be in a comma separated string"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolutions(resolutions)
|
|
87
|
+
|
|
88
|
+
# all resolutions to be tested are defined here
|
|
89
|
+
# they need to be passed as a comma separated string (with or without whitespaces)
|
|
90
|
+
#
|
|
91
|
+
# e.g "400, 800, 1200"
|
|
92
|
+
#
|
|
93
|
+
# if its not a string or the string is empty an exception is raised
|
|
94
|
+
|
|
95
|
+
raise "resolutions for screenshots needs to be a string" unless resolutions.is_a? String
|
|
96
|
+
raise "the resolutions for screenshot cannot be <empty string>" if resolutions == ''
|
|
97
|
+
|
|
98
|
+
# after the base screenshots are taken, the resolutions cannot be changed, an exception would be raised
|
|
99
|
+
|
|
100
|
+
raise_base_screenshots_taken('The resolutions')
|
|
101
|
+
|
|
102
|
+
#we remove whitespaces from the urls, replace ; by , and generate an array of integers
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
@resolutions = clean(resolutions).split(",").map { |s| s.to_i }
|
|
106
|
+
rescue NoMethodError
|
|
107
|
+
raise "resolutions must be in a comma separated string"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def filepath_for_images(path)
|
|
114
|
+
|
|
115
|
+
# if required an absolute path to store all images can be passed here.
|
|
116
|
+
# at the path a file "screenshots" will be generated
|
|
117
|
+
#
|
|
118
|
+
# e.g '/home/finn/pictures/otto'
|
|
119
|
+
#
|
|
120
|
+
# if its not a string or the string is empty an exception is raised
|
|
121
|
+
|
|
122
|
+
raise "path for screenshots needs to be a string" unless path.is_a? String
|
|
123
|
+
raise "the path for the screenshots cannot be <empty string>" if path == ''
|
|
124
|
+
|
|
125
|
+
# after the base screenshots are taken, the path cannot be changed, an exception would be raised
|
|
126
|
+
|
|
127
|
+
raise_base_screenshots_taken('The path')
|
|
128
|
+
|
|
129
|
+
# the path is one string. we just assign the variable
|
|
130
|
+
|
|
131
|
+
@screenshots_path = path
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def use_phantomjs(boolean)
|
|
137
|
+
|
|
138
|
+
# if required the headless environment can we skipped and firefox used for the screenshots
|
|
139
|
+
#
|
|
140
|
+
# e.g use_headless = false
|
|
141
|
+
#
|
|
142
|
+
# if its not a boolean an exception is raised
|
|
143
|
+
|
|
144
|
+
raise "use_headless can only be true or false" unless boolean == !!boolean
|
|
145
|
+
|
|
146
|
+
# after the base screenshots are taken, the browser cannot be changed, an exception would be raised
|
|
147
|
+
|
|
148
|
+
raise_base_screenshots_taken('The browser type (headless)')
|
|
149
|
+
|
|
150
|
+
# sometimes packages are missing on ubuntu to run the headless environment, installing these should resolve it:
|
|
151
|
+
# sudo apt-get install -y xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic xvfb x11-apps imagemagick
|
|
152
|
+
|
|
153
|
+
@headless = boolean
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def difference_path(path)
|
|
159
|
+
|
|
160
|
+
# if required an absolute path to store all difference images can be passed here.
|
|
161
|
+
# in most usecases you may want to save them along with the base and new images
|
|
162
|
+
#
|
|
163
|
+
# e.g '/home/finn/pictures/otto'
|
|
164
|
+
#
|
|
165
|
+
# if its not a string or the string is empty an exception is raised
|
|
166
|
+
|
|
167
|
+
raise "path for difference images needs to be a string" unless path.is_a? String
|
|
168
|
+
raise "the path for the difference images cannot be <empty string>" if path == ''
|
|
169
|
+
|
|
170
|
+
# assign the variable
|
|
171
|
+
|
|
172
|
+
@difference_path = path
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_json_config(path)
|
|
178
|
+
|
|
179
|
+
# loads all possible configs from a json file.
|
|
180
|
+
# in this file all parameters need to be set
|
|
181
|
+
# an example configuration is
|
|
182
|
+
# '{"urls":"/multimedia, /sport","resolutions":"600,800,1200","filepath_for_images":"~/images/","use_headless":true,"difference_path":"#/images/diff"}'
|
|
183
|
+
|
|
184
|
+
#open the file and parse JSON format
|
|
185
|
+
configuration = JSON.parse(File.read(path))
|
|
186
|
+
|
|
187
|
+
# write to method above
|
|
188
|
+
urls(configuration["urls"])
|
|
189
|
+
resolutions(configuration["resolutions"])
|
|
190
|
+
filepath_for_images(configuration["filepath_for_images"])
|
|
191
|
+
use_phantomjs(configuration["use_phantomjs"])
|
|
192
|
+
difference_path(configuration["difference_path"])
|
|
193
|
+
|
|
194
|
+
# the method calls set the variables for the parameters, we return an array with all of them.
|
|
195
|
+
# for the example above it is:
|
|
196
|
+
# [["/multimedia", "/sport"], [600, 800, 1200], "~/images/", true, "#/images/diff"]
|
|
197
|
+
[@urls, @resolutions, @screenshots_path, @headless, @difference_path]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def record_screenshot(version)
|
|
203
|
+
|
|
204
|
+
# to take a screenshot we have all parameters given from the methods above (or set to default values)
|
|
205
|
+
# selenium is started in
|
|
206
|
+
# @headless or firefox
|
|
207
|
+
# and takes a screenshot of the urls
|
|
208
|
+
# @baseurl/@url[0], @baseurl/@url[1], etc...
|
|
209
|
+
# and takes a screenshot for each url for all given resolutions
|
|
210
|
+
# @resolutions[0], @resolutions[1], etc...
|
|
211
|
+
# and saves the screenshot in the file
|
|
212
|
+
# @screenshot_path
|
|
213
|
+
|
|
214
|
+
browser = Browser.new(@baseurl, @urls, @resolutions, @screenshots_path, @headless)
|
|
215
|
+
|
|
216
|
+
# the only argument missing is if this is the "base" or "new" screenshot, this can be
|
|
217
|
+
# passed as an argument. The value does not need to be "base" or "new", but can be anything
|
|
218
|
+
|
|
219
|
+
browser.record(version)
|
|
220
|
+
|
|
221
|
+
# this will close the browser and terminate the headless environment
|
|
222
|
+
|
|
223
|
+
browser.end
|
|
224
|
+
|
|
225
|
+
# this flag is set, so that parameters like resolution or urls cannot be changed any more
|
|
226
|
+
|
|
227
|
+
@got_base_screenshots = true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def compare(base, new)
|
|
233
|
+
|
|
234
|
+
# this compares two previously taken screenshots
|
|
235
|
+
# the "base" and "new" variable need to be the same as previously assigned
|
|
236
|
+
# as "variable" in the method "record_screenshot"!
|
|
237
|
+
# all other information are constants and are passed along
|
|
238
|
+
|
|
239
|
+
@comparer = Comparer.new(base, new, @difference_path, @baseurl, @urls, @resolutions, @screenshots_path)
|
|
240
|
+
|
|
241
|
+
# this gives back an array, which as one element for each difference image.
|
|
242
|
+
# [ {diff_1}, {diff_2}, ...]
|
|
243
|
+
# while each diff is a hash with keys:
|
|
244
|
+
# {url: <url>, width: <width in px>, difference: <%of changed pixel>, base_file: <file path>, new_file: <file path>, diff_file: <file path>}
|
|
245
|
+
|
|
246
|
+
@comparer.difference
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def save_json(path)
|
|
250
|
+
|
|
251
|
+
# json output can be saved if needed. A path is required to save the file
|
|
252
|
+
|
|
253
|
+
raise "screenshots need to be compared before json output can be gernerated" unless @comparer
|
|
254
|
+
|
|
255
|
+
# the array from @comparer.difference is saved as json
|
|
256
|
+
|
|
257
|
+
@comparer.json(path)
|
|
258
|
+
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def raise_base_screenshots_taken(value)
|
|
264
|
+
if @got_base_screenshots
|
|
265
|
+
raise ArgumentError, "#{value} cannot be changed after first set of screenshots were taken"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def clean(urls)
|
|
270
|
+
urls.gsub(' ', '').gsub(';',',')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
end
|
data/lineup.gemspec
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require File.expand_path('../lib/lineup/version', __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |gem|
|
|
4
|
+
gem.authors = %w(Finn Lorbeer)
|
|
5
|
+
gem.version = Lineup::Version
|
|
6
|
+
gem.name = 'lineup'
|
|
7
|
+
gem.platform = Gem::Platform::RUBY
|
|
8
|
+
gem.require_paths = %w(lib)
|
|
9
|
+
gem.license = 'MIT'
|
|
10
|
+
gem.email = %w(finn.von.friesland@googlemail.com)
|
|
11
|
+
gem.summary = "lineup will help you in your automated design regression testing"
|
|
12
|
+
gem.description = %q{lineup takes to screenshots of your app and compares them to references in order to find design flaws in your new code.}
|
|
13
|
+
gem.homepage = 'https://www.otto.de'
|
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
require 'rspec'
|
|
2
|
+
require 'dimensions'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative '../../lib/lineup'
|
|
6
|
+
|
|
7
|
+
describe '#screeshot_recorder' do
|
|
8
|
+
|
|
9
|
+
BASE_URL = 'https://www.otto.de'
|
|
10
|
+
SCREENSHOTS = "#{Dir.pwd}/screenshots/"
|
|
11
|
+
|
|
12
|
+
after(:each) { FileUtils.rmtree SCREENSHOTS }
|
|
13
|
+
|
|
14
|
+
it 'loads all configuration from a json file' do
|
|
15
|
+
# Given
|
|
16
|
+
file = "#{Dir.pwd}/test_configuration.json"
|
|
17
|
+
FileUtils.rm file if (File.exists? file)
|
|
18
|
+
json = '{"urls":"page1, page2","resolutions":"13,42","filepath_for_images":"some/path","use_phantomjs":true,"difference_path":"some/difference/image/path"}'
|
|
19
|
+
save_json(json, file)
|
|
20
|
+
|
|
21
|
+
# When
|
|
22
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
23
|
+
|
|
24
|
+
# Then
|
|
25
|
+
expect(
|
|
26
|
+
lineup.load_json_config(file)
|
|
27
|
+
).to eq([['page1', 'page2'], [13,42], 'some/path', true, 'some/difference/image/path'])
|
|
28
|
+
|
|
29
|
+
# cleanup:
|
|
30
|
+
FileUtils.rm file if (File.exists? file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'opens a URL and takes mobile/tablet/desktop screenshots' do
|
|
34
|
+
# Given
|
|
35
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
36
|
+
|
|
37
|
+
# When
|
|
38
|
+
lineup.record_screenshot('base')
|
|
39
|
+
|
|
40
|
+
# Then
|
|
41
|
+
expect(
|
|
42
|
+
File.exist? ("#{Dir.pwd}/screenshots/base_frontpage_640.png")
|
|
43
|
+
).to be(true)
|
|
44
|
+
# And
|
|
45
|
+
expect(
|
|
46
|
+
File.exist? ("#{Dir.pwd}/screenshots/base_frontpage_800.png")
|
|
47
|
+
).to be(true)
|
|
48
|
+
# And
|
|
49
|
+
expect(
|
|
50
|
+
File.exist? ("#{Dir.pwd}/screenshots/base_frontpage_1180.png")
|
|
51
|
+
).to be(true)
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'takes a screenshot a desired resolution' do
|
|
56
|
+
# Given
|
|
57
|
+
width = '320' #min width firefox as of Sep 2015
|
|
58
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
59
|
+
|
|
60
|
+
# When
|
|
61
|
+
lineup.resolutions(width)
|
|
62
|
+
|
|
63
|
+
# Then
|
|
64
|
+
lineup.record_screenshot('base')
|
|
65
|
+
imagewidth = Dimensions.width("#{Dir.pwd}/screenshots/base_frontpage_#{width}.png")
|
|
66
|
+
expect(
|
|
67
|
+
imagewidth
|
|
68
|
+
).to be < (width.to_i + 10) #depending on the browser:
|
|
69
|
+
# 'width' set the browser to a certain width. The browser itself may then have some frame/border
|
|
70
|
+
# that means, that the viewport is smaller than the width of the browser, thus the image will be a
|
|
71
|
+
# bit smaller then 'width'. To compensate it, we have a +10 here.
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'takes screenshots of different pages, if specified' do
|
|
76
|
+
# Given
|
|
77
|
+
urls = '/, multimedia, sport'
|
|
78
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
79
|
+
lineup.resolutions('1180')
|
|
80
|
+
lineup.urls(urls)
|
|
81
|
+
|
|
82
|
+
# When
|
|
83
|
+
lineup.record_screenshot('base')
|
|
84
|
+
|
|
85
|
+
# Then
|
|
86
|
+
expect(
|
|
87
|
+
File.exist? ("#{Dir.pwd}/screenshots/base_frontpage_1180.png")
|
|
88
|
+
).to be(true)
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
File.exist? ("#{Dir.pwd}/screenshots/base_multimedia_1180.png")
|
|
92
|
+
).to be(true)
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'raises and exception if, parameters are changed after the base screenshot' do
|
|
97
|
+
# Given
|
|
98
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
99
|
+
lineup.urls('/')
|
|
100
|
+
lineup.resolutions('400')
|
|
101
|
+
|
|
102
|
+
# When
|
|
103
|
+
lineup.record_screenshot('base')
|
|
104
|
+
expect{
|
|
105
|
+
lineup.use_phantomjs true
|
|
106
|
+
|
|
107
|
+
# Then
|
|
108
|
+
}.to raise_error ArgumentError
|
|
109
|
+
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'compares a base and a new screenshot and detects no difference if images are the same' do
|
|
113
|
+
# Given
|
|
114
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
115
|
+
lineup.urls('/shoppages/begood')
|
|
116
|
+
lineup.resolutions('400')
|
|
117
|
+
lineup.record_screenshot('base')
|
|
118
|
+
lineup.record_screenshot('new')
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
# When
|
|
122
|
+
lineup.compare('base', 'new')
|
|
123
|
+
|
|
124
|
+
# Then
|
|
125
|
+
).to eq([])
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'compares a base and a new screenshot and returns the difference if the images are NOT the same as json log' do
|
|
130
|
+
# Given
|
|
131
|
+
width = '600'
|
|
132
|
+
base_site = 'multimedia'
|
|
133
|
+
new_site = 'sport'
|
|
134
|
+
json_path = "#{Dir.pwd}"
|
|
135
|
+
json_file = "#{json_path}/log.json"
|
|
136
|
+
|
|
137
|
+
# And Given
|
|
138
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
139
|
+
lineup.urls(base_site)
|
|
140
|
+
lineup.resolutions(width)
|
|
141
|
+
lineup.record_screenshot('base')
|
|
142
|
+
FileUtils.mv "#{Dir.pwd}/screenshots/base_#{base_site}_#{width}.png", "#{Dir.pwd}/screenshots/base_#{new_site}_#{width}.png"
|
|
143
|
+
# change the url and go to a different page, in this way we ensure a conflict and thus a result from the comparison
|
|
144
|
+
lineup = Lineup::Screenshot.new(BASE_URL)
|
|
145
|
+
lineup.urls(new_site)
|
|
146
|
+
lineup.resolutions(width)
|
|
147
|
+
|
|
148
|
+
# When
|
|
149
|
+
lineup.record_screenshot('new')
|
|
150
|
+
|
|
151
|
+
# Then
|
|
152
|
+
# the output will be similar to the values here:
|
|
153
|
+
# [
|
|
154
|
+
# {
|
|
155
|
+
# :url => 'sport',
|
|
156
|
+
# :width => 600,
|
|
157
|
+
# :difference => 0.7340442722738748,
|
|
158
|
+
# :base_file => '/home/myname/lineup/tests/respec/screenshots/base_sport_600.png'
|
|
159
|
+
# :new_file => '/home/myname/lineup/tests/respec/screenshots/new_sport_600.png'
|
|
160
|
+
# :diff_file => '/home/myname/lineup/tests/rspec/screenshots/DIFFERENCE_sport_600.png'
|
|
161
|
+
# }
|
|
162
|
+
# ]
|
|
163
|
+
#
|
|
164
|
+
expect(
|
|
165
|
+
(lineup.compare('base', 'new').first)[:url]
|
|
166
|
+
).to eq('sport')
|
|
167
|
+
# And
|
|
168
|
+
expect(
|
|
169
|
+
(lineup.compare('base', 'new').first)[:width]
|
|
170
|
+
).to eq(600)
|
|
171
|
+
# And
|
|
172
|
+
result = (lineup.compare('base', 'new').first)[:difference]
|
|
173
|
+
expect(
|
|
174
|
+
result
|
|
175
|
+
).to be_within(15).of(20) # 'compare' returns the difference of pixel between the screenshots in %
|
|
176
|
+
# 15-20% of pixel works toady (12.3 on 2015/09) for the difference between sport and multimedia page of OTTO.de,
|
|
177
|
+
# but the pages may some day look more or less alike, then these values can be changed
|
|
178
|
+
# And
|
|
179
|
+
expect(
|
|
180
|
+
(lineup.compare('base', 'new').first)[:base_file]
|
|
181
|
+
).to include("/lineup/tests/rspec/screenshots/base_sport_600.png")
|
|
182
|
+
# And
|
|
183
|
+
expect(
|
|
184
|
+
(lineup.compare('base', 'new').first)[:new_file]
|
|
185
|
+
).to include("/lineup/tests/rspec/screenshots/new_sport_600.png")
|
|
186
|
+
# And
|
|
187
|
+
expect(
|
|
188
|
+
(lineup.compare('base', 'new').first)[:difference_file]
|
|
189
|
+
).to include("/lineup/tests/rspec/screenshots/DIFFERENCE_sport_600.png")
|
|
190
|
+
|
|
191
|
+
# And When
|
|
192
|
+
lineup.save_json(json_path)
|
|
193
|
+
|
|
194
|
+
# Then
|
|
195
|
+
expect(
|
|
196
|
+
File.exist? json_file
|
|
197
|
+
).to be(true)
|
|
198
|
+
# And
|
|
199
|
+
expect(
|
|
200
|
+
File.read json_file
|
|
201
|
+
).to include("\"difference\":#{result},")
|
|
202
|
+
|
|
203
|
+
# cleanup:
|
|
204
|
+
FileUtils.rm json_file if (File.exists? json_file)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def save_json(json, file)
|
|
210
|
+
file = File.open(
|
|
211
|
+
file, 'a'
|
|
212
|
+
)
|
|
213
|
+
file.write(json)
|
|
214
|
+
file.close
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lineup
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Finn
|
|
8
|
+
- Lorbeer
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2015-09-24 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: lineup takes to screenshots of your app and compares them to references
|
|
15
|
+
in order to find design flaws in your new code.
|
|
16
|
+
email:
|
|
17
|
+
- finn.von.friesland@googlemail.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- ".gitignore"
|
|
23
|
+
- Gemfile
|
|
24
|
+
- Gemfile.lock
|
|
25
|
+
- README.md
|
|
26
|
+
- bin/lineup
|
|
27
|
+
- doc/example.png
|
|
28
|
+
- lib/controller/browser.rb
|
|
29
|
+
- lib/controller/comparer.rb
|
|
30
|
+
- lib/helper.rb
|
|
31
|
+
- lib/lineup.rb
|
|
32
|
+
- lib/lineup/version.rb
|
|
33
|
+
- lineup.gemspec
|
|
34
|
+
- tests/rspec/lineup_spec.rb
|
|
35
|
+
homepage: https://www.otto.de
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata: {}
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubyforge_project:
|
|
55
|
+
rubygems_version: 2.4.8
|
|
56
|
+
signing_key:
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: lineup will help you in your automated design regression testing
|
|
59
|
+
test_files: []
|