lineup 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Example view of base (left), new (right) as well as diff image.](doc/example.png)
|
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: []
|