├── doc ├── lineup.png ├── example.png └── lineup_mini.png ├── .gitignore ├── lib ├── lineup │ └── version.rb ├── helper.rb ├── controller │ ├── comparer.rb │ └── browser.rb └── lineup.rb ├── Gemfile ├── bin └── lineup ├── lineup.gemspec ├── Gemfile.lock ├── README.md └── tests └── rspec └── lineup_spec.rb /doc/lineup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de-legacy/lineup/master/doc/lineup.png -------------------------------------------------------------------------------- /doc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de-legacy/lineup/master/doc/example.png -------------------------------------------------------------------------------- /doc/lineup_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de-legacy/lineup/master/doc/lineup_mini.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/images/** 2 | .idea/ 3 | out.html 4 | screenshots/ 5 | *.gem 6 | 7 | *~ 8 | chromedriver.log 9 | .ruby-version 10 | .DS_Store 11 | .kate-swp 12 | .ruby-gemset 13 | .swp 14 | -------------------------------------------------------------------------------- /lib/lineup/version.rb: -------------------------------------------------------------------------------- 1 | module Lineup 2 | class Version 3 | MAJOR = 0 4 | MINOR = 7 5 | PATCH = 3 6 | 7 | class << self 8 | def to_s 9 | [MAJOR, MINOR, PATCH].join('.') 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rspec' 6 | gem 'pxdoppelganger', '0.1.1' 7 | gem 'selenium-webdriver' 8 | gem 'watir-webdriver' 9 | gem 'watir' 10 | gem 'headless' 11 | gem 'dimensions' 12 | gem 'oily_png', '1.2.0' 13 | -------------------------------------------------------------------------------- /lib/helper.rb: -------------------------------------------------------------------------------- 1 | module Helper 2 | extend self 3 | 4 | def filename(path, url, width, version) 5 | "#{path}/#{name(url)}_#{width}_#{version}.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 -------------------------------------------------------------------------------- /bin/lineup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require '../lib/lineup' 4 | 5 | puts("This is an example. And a benchmark.") 6 | 7 | lineup = Lineup::Screenshot.new('https://google.de') 8 | lineup.resolutions("600,800,1200") 9 | lineup.use_phantomjs false 10 | lineup.wait_for_asynchron_pages(2) 11 | 12 | puts("Taking base screenshots") 13 | start = Time.now 14 | lineup.record_screenshot('base') 15 | screenshots = Time.now 16 | 17 | puts("Taking new screenshots") 18 | lineup.record_screenshot('new') 19 | screenshots2 = Time.now 20 | 21 | puts("Starting comparison") 22 | lineup.compare('base', 'new') 23 | lineup.save_json(".") 24 | compare = Time.now 25 | 26 | s_t = screenshots - start 27 | puts("Screenshots first run took #{s_t} seconds") 28 | s_t = screenshots2 - screenshots 29 | puts("Screenshots second run took #{s_t} seconds") 30 | c = compare - screenshots2 31 | puts("Comparison took #{c} seconds") 32 | 33 | puts("End") 34 | -------------------------------------------------------------------------------- /lineup.gemspec: -------------------------------------------------------------------------------- 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 | gem.add_dependency 'rspec', '~>3.2' 17 | gem.add_dependency 'pxdoppelganger', '~>0.1' 18 | gem.add_dependency 'selenium-webdriver', '~>2.46' 19 | gem.add_dependency 'watir-webdriver', '~>0.8' 20 | gem.add_dependency 'watir', '~>5.0' 21 | gem.add_dependency 'headless', '~>0.1' 22 | gem.add_dependency 'dimensions', '~>1.3' 23 | gem.add_dependency 'chunky_png', '<=1.3.6' 24 | gem.add_dependency 'oily_png', '~>1.2' 25 | 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | lineup (0.7.3) 5 | chunky_png (<= 1.3.6) 6 | dimensions (~> 1.3) 7 | headless (~> 0.1) 8 | oily_png (~> 1.2) 9 | pxdoppelganger (~> 0.1) 10 | rspec (~> 3.2) 11 | selenium-webdriver (~> 2.46) 12 | watir (~> 5.0) 13 | watir-webdriver (~> 0.8) 14 | 15 | GEM 16 | remote: https://rubygems.org/ 17 | specs: 18 | childprocess (0.5.9) 19 | ffi (~> 1.0, >= 1.0.11) 20 | chunky_png (1.3.6) 21 | commonwatir (4.0.0) 22 | diff-lcs (1.2.5) 23 | dimensions (1.3.0) 24 | ffi (1.9.14) 25 | headless (0.3.1) 26 | oily_png (1.2.0) 27 | chunky_png (~> 1.3.1) 28 | pxdoppelganger (0.1.1) 29 | rspec (3.5.0) 30 | rspec-core (~> 3.5.0) 31 | rspec-expectations (~> 3.5.0) 32 | rspec-mocks (~> 3.5.0) 33 | rspec-core (3.5.3) 34 | rspec-support (~> 3.5.0) 35 | rspec-expectations (3.5.0) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.5.0) 38 | rspec-mocks (3.5.0) 39 | diff-lcs (>= 1.2.0, < 2.0) 40 | rspec-support (~> 3.5.0) 41 | rspec-support (3.5.0) 42 | rubyzip (1.2.0) 43 | selenium-webdriver (2.53.4) 44 | childprocess (~> 0.5) 45 | rubyzip (~> 1.0) 46 | websocket (~> 1.0) 47 | watir (5.0.0) 48 | commonwatir (~> 4) 49 | watir-webdriver 50 | watir-webdriver (0.9.3) 51 | selenium-webdriver (>= 2.46.2) 52 | websocket (1.2.3) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | dimensions 59 | headless 60 | lineup! 61 | oily_png (= 1.2.0) 62 | pxdoppelganger (= 0.1.1) 63 | rspec 64 | selenium-webdriver 65 | watir 66 | watir-webdriver 67 | 68 | BUNDLED WITH 69 | 1.12.5 70 | -------------------------------------------------------------------------------- /lib/controller/comparer.rb: -------------------------------------------------------------------------------- 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 | private 21 | 22 | def compare_images 23 | self.difference = [] 24 | @urls.each do |url| 25 | @resolutions.each do |width| 26 | base_name = Helper.filename( 27 | @absolute_image_path, 28 | url, 29 | width, 30 | @base 31 | ) 32 | new_name = Helper.filename( 33 | @absolute_image_path, 34 | url, 35 | width, 36 | @new 37 | ) 38 | images = PXDoppelganger::Images.new( 39 | base_name, 40 | new_name 41 | ) 42 | if images.difference > 1e-03 # for changes bigger than 1 per 1.000; otherwise we see mathematical artifacts 43 | diff_name = Helper.filename( 44 | @difference_path, 45 | url, 46 | width, 47 | 'DIFFERENCE' 48 | ) 49 | images.save_difference_image diff_name 50 | result = { 51 | url: url, 52 | width: width, 53 | difference: images.difference, 54 | base_file: base_name, 55 | new_file: new_name, 56 | difference_file: diff_name 57 | } 58 | self.difference << result 59 | end 60 | end 61 | end 62 | end 63 | 64 | end -------------------------------------------------------------------------------- /lib/controller/browser.rb: -------------------------------------------------------------------------------- 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, wait, cookies = [], localStorage = []) 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 | @wait = wait 18 | 19 | if cookies 20 | @cookies = cookies 21 | else 22 | @cookies = [] 23 | end 24 | 25 | if localStorage 26 | @localStorage = localStorage 27 | else 28 | @localStorage = [] 29 | end 30 | end 31 | 32 | def record(version) 33 | browser_loader 34 | @urls.each do |url| 35 | @resolutions.each do |width| 36 | screenshot_recorder(width, url, version) 37 | end 38 | end 39 | end 40 | 41 | def end 42 | begin #Timeout::Error 43 | Timeout::timeout(10) { @browser.close } 44 | rescue Timeout::Error 45 | browser_pid = @browser.driver.instance_variable_get(:@bridge).instance_variable_get(:@service).instance_variable_get(:@process).pid 46 | ::Process.kill('KILL', browser_pid) 47 | sleep 1 48 | end 49 | sleep 5 # to prevent xvfb to freeze 50 | end 51 | 52 | private 53 | 54 | def browser_loader 55 | if @headless 56 | @browser = Watir::Browser.new :phantomjs 57 | else 58 | @browser = Watir::Browser.new :firefox 59 | end 60 | end 61 | 62 | def screenshot_recorder(width, url, version) 63 | filename = Helper.filename(@absolute_image_path, url, width, version) 64 | @browser.driver.manage.window.resize_to(width, 1000) 65 | 66 | url = Helper.url(@baseurl, url) 67 | 68 | if @cookies.any? || @localStorage.any? 69 | # load url first before setting cookies and/or localStorage values 70 | @browser.goto url 71 | 72 | if @cookies.any? 73 | @browser.cookies.clear 74 | @cookies.each do |cookie| 75 | @browser.cookies.add(cookie[:name], cookie[:value], domain: cookie[:domain], path: cookie[:path], expires: Time.now + 7200, secure: cookie[:secure]) 76 | end 77 | end 78 | 79 | if @localStorage.any? 80 | @localStorage.each do |key, value| 81 | # Generate javascript for localStorage.setItem, escaping single quotes in key and value 82 | stmt = "localStorage.setItem('" + key.gsub("'", "\\\\'") + "','" + value.gsub("'", "\\\\'") + "')"; 83 | @browser.execute_script(stmt) 84 | end 85 | end 86 | end 87 | 88 | @browser.goto url 89 | 90 | sleep @wait if @wait 91 | @browser.screenshot.save( File.expand_path(filename)) 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lineup 2 | 3 | ![Logo](https://raw.githubusercontent.com/otto-de/lineup/master/doc/lineup_mini.png) 4 | 5 | Lineup is doing automated testing of webpage designs, eg. in continious delivery. 6 | If you push new code to production, you can evaluate the design of your page compared to a defined base design and 7 | get an analysis about the difference of the designs: 8 | 9 | For all images that you want to compare, you will receive information about how many pixel are different 10 | between the two image version and an image, that contains only the parts, that changed, a "difference image". 11 | 12 | ![Example view of base (left), new (right) as well as diff image.](doc/example.png) 13 | Picture: Example view of base (left), new (right) as well as diff image. In this example, the margin around the bottom headline increased, 14 | thus some of the elements moved down. 15 | 16 | ## Requirements 17 | 18 | A firefox browser must be installed, as well as phantomjs. 19 | 20 | ## Usage 21 | 22 | Add it into your Gemfile: 23 | ````ruby 24 | gem "lineup" 25 | ```` 26 | 27 | Or install it manually with the following command: 28 | ```` 29 | gem install lineup 30 | ```` 31 | 32 | Do a base reference screenshot of you application: 33 | ````ruby 34 | require 'lineup' 35 | lineup = Lineup::Screenshot.new('https://www.otto.de') 36 | lineup.record_screenshot('base') 37 | ```` 38 | 39 | Do something (deployment of your new code) and take a new screenshot 40 | ````ruby 41 | lineup.record_screenshot('new') 42 | ```` 43 | 44 | Analyse the results: 45 | ````ruby 46 | lineup.compare('new', 'base') 47 | => [{:url => 'sport', :width => 600, :difference => 0.7340442722738748, 48 | :base_file => '/home/name/lineup/screenshots/frontpage_600_base.png' 49 | :new_file => '/home/name/lineup/screenshots/frontpage_600_new.png' 50 | :diff_file => '/home/name/lineup/screenshots/frontpage_600_DIFFERENCE.png' }] 51 | ```` 52 | 53 | You can save it for later use: 54 | ````ruby 55 | lineup.save_json('/home/name/lineup/results/') 56 | => '/home/name/lineup/results/log.json' 57 | ```` 58 | 59 | ## More configuration: 60 | 61 | There are multiple ways to specify what to lineup and compare. 62 | 63 | By specifying different urls via ````#urls````: 64 | ````ruby 65 | lineup = Lineup::Screenshot.new('https://www.otto.de') 66 | lineup.urls('/, /multimedia, /sport') 67 | ```` 68 | This will do analysis of otto.de root (frontpage), otto.de/multimedia and otto.de/sport. 69 | It requires a comma separated string. Default value is only root. 70 | 71 | By specifying different resolutions via ````#resolutions````: 72 | ````ruby 73 | lineup = Lineup::Screenshot.new('https://www.otto.de') 74 | lineup.resolutions('600, 800, 1200') 75 | ```` 76 | The values are the browser width in pixel. For each size an analysis is done. 77 | It require a comma separated string. Default values are 640px, 800px and 1180px. 78 | 79 | By specifying a wait time for asychron elements on a page via ````#wait_for_asynchron_pages````: 80 | ````ruby 81 | lineup = Lineup::Screenshot.new('https://www.otto.de') 82 | lineup.wait_for_asynchron_pages(5) 83 | ```` 84 | The wait time is the time in seconds lineup will wait after the page load before it takes a screenshot. 85 | In this time third party tools, AJAX or js as well as fonts can be (lazy) loaded. The wait time has no upper limit. 86 | 87 | By specifying a filepath for the screenshots via ````#filepath_for_images````: 88 | ````ruby 89 | lineup = Lineup::Screenshot.new('https://www.otto.de') 90 | lineup.filepath_for_images('/home/myname/lineup/screenshots') 91 | ```` 92 | Creates a file and saves the screenshots in the file. Default is ````"#{Dir.pwd}/screenshots"```` 93 | 94 | By specifying a filepath for the difference image via ````#difference_path````: 95 | ````ruby 96 | lineup = Lineup::Screenshot.new('https://www.otto.de') 97 | lineup.difference_path('/home/myname/lineup/result') 98 | ```` 99 | Creates a file and saves the difference image in the file. Default is ````"#{Dir.pwd}/screenshots"```` 100 | 101 | By specifying wether or not to use phantomjs via ````#use_phantomjs````: 102 | ````ruby 103 | lineup = Lineup::Screenshot.new('https://www.otto.de') 104 | lineup.use_phantomjs(true) 105 | ```` 106 | If ````false```` the screenshots are taken in Firefox. ````#load_json_config````: 107 | 108 | Load all above configs from a json file via 109 | ````ruby 110 | lineup = Lineup::Screenshot.new('https://www.otto.de') 111 | lineup.load_json_config('/home/myname/lineup/config.json') 112 | ```` 113 | While my file contains all relevant information 114 | ````json 115 | { 116 | "urls":"/multimedia, /sport", 117 | "resolutions":"600,800,1200", 118 | "filepath_for_images":"~/images/", 119 | "use_phantomjs":true, 120 | "difference_path":"#/images/diff", 121 | "wait_for_asynchron_pages":5 122 | } 123 | ```` 124 | However, if the configuration is done with a json object, it needs to contain all information, there 125 | is no optional parameter. 126 | 127 | ## Example: 128 | 129 | ````ruby 130 | base_name = 'name-for-base-screenshot' 131 | new_name = 'name-for-new-screenshot' 132 | urls = '/, multimedia, sport' 133 | resolutions = '600, 800, 1200' 134 | images_path = '/home/myname/lineup/screenshots' 135 | difference_path = '/home/myname/lineup/results' 136 | json_path = 'home/myname/lineup/results' 137 | phantomjs = true 138 | 139 | lineup = Lineup::Screenshot.new('https://www.otto.de') 140 | lineup.urls(urls) 141 | lineup.resolutions(resolutions) 142 | lineup.filepath_for_images(images_path 143 | lineup.difference_path(difference_path) 144 | lineup.use_phantomjs(phantomjs) 145 | 146 | lineup.record_screenshot(base_name) 147 | # do sth. (eg. deploy new software) 148 | lineup.record_screenshot(new_name) 149 | lineup.save_json(json_path) 150 | ```` 151 | Now open home/myname/lineup/results and find: 152 | the difference files and a log.json with all information about what images are not the same. 153 | 154 | ## Contribute 155 | 156 | Please do! 157 | 158 | -------------------------------------------------------------------------------- /tests/rspec/lineup_spec.rb: -------------------------------------------------------------------------------- 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.google.de' 10 | SCREENSHOTS = "#{Dir.pwd}/screenshots/" 11 | 12 | after(:each) { FileUtils.rmtree SCREENSHOTS } 13 | 14 | it 'loads minimum 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", 19 | "resolutions": "13", 20 | "filepath_for_images":"screenshots/path", 21 | "use_phantomjs":true, 22 | "difference_path":"screenshots/path/difference", 23 | "wait_for_asynchron_pages":5 24 | }' 25 | save_json(json, file) 26 | 27 | # When 28 | lineup = Lineup::Screenshot.new(BASE_URL) 29 | 30 | # Then 31 | expect( 32 | lineup.load_json_config(file) 33 | ).to eq([['page1'], [13], 'screenshots/path', true, 'screenshots/path/difference', 5, [], {}]) 34 | 35 | # cleanup: 36 | FileUtils.rm file if (File.exists? file) 37 | end 38 | 39 | it 'loads all configuration from a json file' do 40 | # Given 41 | file = "#{Dir.pwd}/test_configuration.json" 42 | FileUtils.rm file if (File.exists? file) 43 | json = '{"urls":"page1, page2", 44 | "resolutions":"13,42", 45 | "filepath_for_images":"screenshots/path", 46 | "use_phantomjs":true, 47 | "difference_path":"screenshots/path/difference", 48 | "wait_for_asynchron_pages":5, 49 | "cookies" : [{ 50 | "name":"cookie2", 51 | "value":"22222", 52 | "domain":".google.de", 53 | "path":"/", 54 | "secure":false 55 | }, 56 | { 57 | "name":"cookie3", 58 | "value":"33333", 59 | "domain":".google.de", 60 | "path":"/", 61 | "secure":false 62 | }], 63 | "localStorage":{"myKey1":"myValue1","myKey2":"myValue2"} 64 | }' 65 | save_json(json, file) 66 | 67 | # When 68 | lineup = Lineup::Screenshot.new(BASE_URL) 69 | 70 | # Then 71 | expect( 72 | lineup.load_json_config(file) 73 | ).to eq([['page1', 'page2'], [13,42], 'screenshots/path', true, 'screenshots/path/difference', 5, 74 | [{:name=>"cookie2", :value=>"22222", :domain=>".google.de", :path=>"/", :secure=>false}, 75 | {:name=>"cookie3", :value=>"33333", :domain=>".google.de", :path=>"/", :secure=>false}], 76 | {"myKey1"=>"myValue1", "myKey2"=>"myValue2"}]) 77 | 78 | # cleanup: 79 | FileUtils.rm file if (File.exists? file) 80 | end 81 | 82 | it 'opens a URL and takes mobile/tablet/desktop screenshots' do 83 | # Given 84 | lineup = Lineup::Screenshot.new(BASE_URL) 85 | 86 | # When 87 | lineup.record_screenshot('base') 88 | 89 | # Then 90 | expect( 91 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_640_base.png") 92 | ).to be(true) 93 | # And 94 | expect( 95 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_800_base.png") 96 | ).to be(true) 97 | # And 98 | expect( 99 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_1180_base.png") 100 | ).to be(true) 101 | 102 | end 103 | 104 | it 'opens a URL and takes mobile/tablet/desktop screenshots using firefox' do 105 | # Given 106 | lineup = Lineup::Screenshot.new(BASE_URL) 107 | lineup.use_phantomjs false 108 | 109 | # When 110 | lineup.record_screenshot('base') 111 | 112 | # Then 113 | expect( 114 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_640_base.png") 115 | ).to be(true) 116 | # And 117 | expect( 118 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_800_base.png") 119 | ).to be(true) 120 | # And 121 | expect( 122 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_1180_base.png") 123 | ).to be(true) 124 | 125 | end 126 | 127 | it 'takes a screenshot at desired resolution' do 128 | # Given 129 | width = '700' 130 | lineup = Lineup::Screenshot.new(BASE_URL) 131 | 132 | # When 133 | lineup.resolutions(width) 134 | 135 | # Then 136 | lineup.record_screenshot('base') 137 | imagewidth = Dimensions.width("#{Dir.pwd}/screenshots/frontpage_#{width}_base.png") 138 | expect( 139 | imagewidth 140 | ).to be < (width.to_i + 10) #depending on the browser: 141 | # 'width' set the browser to a certain width. The browser itself may then have some frame/border 142 | # that means, that the viewport is smaller than the width of the browser, thus the image will be a 143 | # bit smaller then 'width'. To compensate it, we have a +10 here. 144 | 145 | end 146 | 147 | it 'takes screenshots of different pages, if specified' do 148 | # Given 149 | urls = '/, flights' 150 | lineup = Lineup::Screenshot.new(BASE_URL) 151 | lineup.resolutions('1180') 152 | lineup.urls(urls) 153 | 154 | # When 155 | lineup.record_screenshot('base') 156 | 157 | # Then 158 | expect( 159 | File.exist? ("#{Dir.pwd}/screenshots/frontpage_1180_base.png") 160 | ).to be(true) 161 | 162 | expect( 163 | File.exist? ("#{Dir.pwd}/screenshots/flights_1180_base.png") 164 | ).to be(true) 165 | 166 | end 167 | 168 | it 'raises and exception if parameters are changed after the base screenshot' do 169 | # Given 170 | lineup = Lineup::Screenshot.new(BASE_URL) 171 | lineup.urls('/') 172 | lineup.resolutions('400') 173 | 174 | # When 175 | lineup.record_screenshot('base') 176 | expect{ 177 | lineup.use_phantomjs true 178 | 179 | # Then 180 | }.to raise_error ArgumentError 181 | end 182 | 183 | it 'compares a base and a new screenshot and detects no difference if images are the same' do 184 | # Given 185 | lineup = Lineup::Screenshot.new(BASE_URL) 186 | lineup.urls('/') 187 | lineup.resolutions('400') 188 | lineup.wait_for_asynchron_pages(5) 189 | lineup.use_phantomjs(true) 190 | cookie = [{"name" => "CONSENT", 191 | "value" => "YES+DE.de+V7", 192 | "domain" => ".google.de", 193 | "path" => "/", 194 | "secure" => false}] 195 | lineup.cookies(cookie) 196 | 197 | lineup.record_screenshot('base') 198 | lineup.record_screenshot('new') 199 | 200 | expect( 201 | # When 202 | lineup.compare('base', 'new') 203 | 204 | # Then 205 | ).to eq([]) 206 | 207 | end 208 | 209 | it 'compares a base and a new screenshot when loading a json config and setting a cookie' do 210 | # Given 211 | file = "#{Dir.pwd}/test_configuration.json" 212 | FileUtils.rm file if (File.exists? file) 213 | 214 | json = '{"urls":"page1, page2", 215 | "resolutions":"13,42", 216 | "filepath_for_images":"screenshots/path", 217 | "use_phantomjs":true, 218 | "difference_path":"screenshots/path/difference", 219 | "wait_for_asynchron_pages":5, 220 | "cookies" : [{ 221 | "name":"cookie2", 222 | "value":"22222", 223 | "domain":".google.de", 224 | "path":"/", 225 | "secure":false 226 | }, 227 | { 228 | "name":"cookie3", 229 | "value":"33333", 230 | "domain":".google.de", 231 | "path":"/", 232 | "secure":false 233 | }] 234 | }' 235 | save_json(json, file) 236 | lineup = Lineup::Screenshot.new(BASE_URL) 237 | lineup.load_json_config(file) 238 | 239 | lineup.record_screenshot('base') 240 | lineup.record_screenshot('new') 241 | 242 | expect( 243 | # When 244 | lineup.compare('base', 'new') 245 | 246 | # Then 247 | ).to eq([]) 248 | 249 | # cleanup: 250 | FileUtils.rm file if (File.exists? file) 251 | 252 | end 253 | 254 | it 'takes a screenshot when loading a json config and setting local storage key value pair containing single quotes' do 255 | # Given 256 | file = "#{Dir.pwd}/test_configuration.json" 257 | FileUtils.rm file if (File.exists? file) 258 | 259 | json = '{"urls":"page1", 260 | "resolutions":"200", 261 | "filepath_for_images":"screenshots/path", 262 | "use_phantomjs":true, 263 | "difference_path":"screenshots/path/difference", 264 | "wait_for_asynchron_pages":5, 265 | "localStorage": {"{\'mySpecialKey\'}":"{\'myvalue\':{\'value\':test,\'timestamp\':1467723066092}}"} 266 | }' 267 | save_json(json, file) 268 | lineup = Lineup::Screenshot.new(BASE_URL) 269 | lineup.load_json_config(file) 270 | 271 | lineup.record_screenshot('base') 272 | # expect: no exception 273 | 274 | # cleanup: 275 | FileUtils.rm file if (File.exists? file) 276 | 277 | end 278 | 279 | it 'compares a base and a new screenshot when loading a json config and setting local storage key value pairs' do 280 | # Given 281 | file = "#{Dir.pwd}/test_configuration.json" 282 | FileUtils.rm file if (File.exists? file) 283 | 284 | json = '{"urls":"page1", 285 | "resolutions":"200", 286 | "filepath_for_images":"screenshots/path", 287 | "use_phantomjs":true, 288 | "difference_path":"screenshots/path/difference", 289 | "wait_for_asynchron_pages":5, 290 | "localStorage":{"\'myKey\'":"{\'myValue\'}","myKey2":"myValue2"} 291 | }' 292 | save_json(json, file) 293 | lineup = Lineup::Screenshot.new(BASE_URL) 294 | lineup.load_json_config(file) 295 | 296 | lineup.record_screenshot('base') 297 | lineup.record_screenshot('new') 298 | 299 | expect( 300 | # When 301 | lineup.compare('base', 'new') 302 | 303 | # Then 304 | ).to eq([]) 305 | 306 | # cleanup: 307 | FileUtils.rm file if (File.exists? file) 308 | 309 | end 310 | 311 | it 'compares a base and a new screenshot and returns the difference if the images are NOT the same as json log' do 312 | # Given 313 | width = '800' 314 | base_site = '?q=test' 315 | new_site = '?q=somethingelse' 316 | json_path = "#{Dir.pwd}" 317 | json_file = "#{json_path}/log.json" 318 | 319 | # And Given 320 | lineup = Lineup::Screenshot.new(BASE_URL) 321 | lineup.urls(base_site) 322 | lineup.resolutions(width) 323 | lineup.record_screenshot('base') 324 | FileUtils.mv "#{Dir.pwd}/screenshots/#{base_site}_#{width}_base.png", "#{Dir.pwd}/screenshots/#{new_site}_#{width}_base.png" 325 | # change the url and go to a different page, in this way we ensure a conflict and thus a result from the comparison 326 | lineup = Lineup::Screenshot.new(BASE_URL) 327 | lineup.urls(new_site) 328 | lineup.resolutions(width) 329 | 330 | # When 331 | lineup.record_screenshot('new') 332 | 333 | # Then 334 | # the output will be similar to the values here: 335 | # [ 336 | # { 337 | # :url => 'translate', 338 | # :width => 800, 339 | # :difference => 0.7340442722738748, 340 | # :base_file => '/home/myname/lineup/tests/respec/screenshots/translate_600_base.png' 341 | # :new_file => '/home/myname/lineup/tests/respec/screenshots/translate_600_new.png' 342 | # :diff_file => '/home/myname/lineup/tests/rspec/screenshots/translate_600_DIFFERENCE.png' 343 | # } 344 | # ] 345 | # 346 | expect( 347 | (lineup.compare('base', 'new').first)[:url] 348 | ).to eq('?q=somethingelse') 349 | # And 350 | expect( 351 | (lineup.compare('base', 'new').first)[:width] 352 | ).to eq(800) 353 | # And 354 | result = (lineup.compare('base', 'new').first)[:difference] 355 | expect( 356 | result 357 | ).to be > 0 # 'compare' returns the difference of pixel between the screenshots in % 358 | expect( 359 | (lineup.compare('base', 'new').first)[:base_file] 360 | ).to include("/lineup/tests/rspec/screenshots/?q=somethingelse_#{width}_base.png") 361 | # And 362 | expect( 363 | (lineup.compare('base', 'new').first)[:new_file] 364 | ).to include("/lineup/tests/rspec/screenshots/?q=somethingelse_#{width}_new.png") 365 | # And 366 | expect( 367 | (lineup.compare('base', 'new').first)[:difference_file] 368 | ).to include("/lineup/tests/rspec/screenshots/?q=somethingelse_#{width}_DIFFERENCE.png") 369 | 370 | # And When 371 | lineup.save_json(json_path) 372 | 373 | # Then 374 | expect( 375 | File.exist? json_file 376 | ).to be(true) 377 | # And 378 | expect( 379 | File.read json_file 380 | ).to include("\"difference\":#{result},") 381 | 382 | # cleanup: 383 | FileUtils.rm json_file if (File.exists? json_file) 384 | end 385 | 386 | private 387 | 388 | def save_json(json, file) 389 | file = File.open( 390 | file, 'a' 391 | ) 392 | file.write(json) 393 | file.close 394 | end 395 | 396 | end -------------------------------------------------------------------------------- /lib/lineup.rb: -------------------------------------------------------------------------------- 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 | 55 | @comparer = [] 56 | end 57 | 58 | 59 | def urls(urls) 60 | 61 | # all urls to be tested are defined here 62 | # they need to be passed as a comma separated string (with or without whitespaces) 63 | # 64 | # e.g "/, /multimedia, /sport" 65 | # 66 | # the pages are used to name the image files, too 67 | # 68 | # if it is not a string or the string is empty an exception is raised 69 | 70 | raise "url for screenshots needs to be a string" unless urls.is_a? String 71 | raise "url for screenshots cannot be " if urls == '' 72 | 73 | # after the base screenshots are taken, the urls cannot be changed, an exception would be raised 74 | 75 | raise_base_screenshots_taken('The urls') 76 | 77 | #we remove whitespaces from the urls, replace ; by , and generate an array, splitted by comma 78 | 79 | begin 80 | @urls= clean(urls).split(",") 81 | rescue NoMethodError 82 | raise "urls must be in a comma separated string" 83 | end 84 | end 85 | 86 | 87 | def resolutions(resolutions) 88 | 89 | # all resolutions to be tested are defined here 90 | # they need to be passed as a comma separated string (with or without whitespaces) 91 | # 92 | # e.g "400, 800, 1200" 93 | # 94 | # if its not a string or the string is empty an exception is raised 95 | 96 | raise "resolutions for screenshots needs to be a string" unless resolutions.is_a? String 97 | raise "the resolutions for screenshot cannot be " if resolutions == '' 98 | 99 | # after the base screenshots are taken, the resolutions cannot be changed, an exception would be raised 100 | 101 | raise_base_screenshots_taken('The resolutions') 102 | 103 | #we remove whitespaces from the urls, replace ; by , and generate an array of integers 104 | 105 | begin 106 | @resolutions = clean(resolutions).split(",").map { |s| s.to_i } 107 | rescue NoMethodError 108 | raise "resolutions must be in a comma separated string" 109 | end 110 | end 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 " 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 | def use_phantomjs(boolean) 136 | 137 | # if required the headless environment can we skipped and firefox used for the screenshots 138 | # 139 | # e.g use_headless = false 140 | # 141 | # if its not a boolean an exception is raised 142 | 143 | raise "use_headless can only be true or false" unless boolean == !!boolean 144 | 145 | # after the base screenshots are taken, the browser cannot be changed, an exception would be raised 146 | 147 | raise_base_screenshots_taken('The browser type (headless)') 148 | 149 | # sometimes packages are missing on ubuntu to run the headless environment, installing these should resolve it: 150 | # sudo apt-get install -y xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic xvfb x11-apps imagemagick 151 | 152 | @headless = boolean 153 | end 154 | 155 | 156 | def difference_path(path) 157 | 158 | # if required an absolute path to store all difference images can be passed here. 159 | # in most usecases you may want to save them along with the base and new images 160 | # 161 | # e.g '/home/finn/pictures/otto' 162 | # 163 | # if its not a string or the string is empty an exception is raised 164 | 165 | raise "path for difference images needs to be a string" unless path.is_a? String 166 | raise "the path for the difference images cannot be " if path == '' 167 | 168 | # assign the variable 169 | 170 | @difference_path = path 171 | end 172 | 173 | def cookies(cookies) 174 | 175 | # a hash for cookies can be set here. this is optional. 176 | # 177 | # e.g {name: 'name', value: 'value', domain: 'domain.com', path: '/', expires: