├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE-MIT ├── README.md ├── examples ├── .gitignore ├── README.md ├── webdrivercss.browserstack.js └── webdrivercss.browserstack.with.mocha.js ├── gruntfile.js ├── index.js ├── lib ├── asyncCallback.js ├── compareImages.js ├── cropImage.js ├── documentScreenshot.js ├── endSession.js ├── exclude.js ├── getPageInfo.js ├── helpers │ └── generateUUID.js ├── logWarning.js ├── makeScreenshot.js ├── renameFiles.js ├── saveImageDiff.js ├── scripts │ ├── getPageInfo.js │ ├── getScreenDimension.js │ ├── getScrollingPosition.js │ └── scroll.js ├── setScreenWidth.js ├── startSession.js ├── syncImages.js ├── viewportScreenshot.js ├── webdrivercss.js └── workflow.js ├── package.json └── test ├── bootstrap.js ├── fixtures ├── comparisonTest.diff.png ├── excludeElem.png ├── hideElem.png ├── notWithinViewport.png ├── testAtSpecificPosition.current.png ├── testWithGivenElement.current.png ├── testWithGivenElementAndWidthHeight.current.png ├── testWithWidthHeightParameter.current.png ├── testWithoutParameter.current.png ├── timeoutTest.current.png ├── timeoutTestWorking.current.png └── webdrivercss.tar.gz ├── mocha.opts ├── site ├── .bowerrc ├── bower.json ├── css │ ├── bootstrap.css │ └── main.css ├── four.html ├── gestureTest.html ├── images │ └── webdriver-robot.png ├── index.html ├── three.html └── two.html └── spec ├── .DS_Store ├── exclude.js ├── hide.js ├── imageCapturing.js ├── imageComparison.js ├── instantiation.js ├── remove.js ├── screenWidth.js └── sync.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "indent": [2, 4] 5 | }, 6 | "parser": "babel-eslint", 7 | "globals": {} 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | test/site/components 27 | 28 | #jetbrains IDE 29 | .idea 30 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailing": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "latedef": true, 7 | "laxcomma": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "sub": true, 11 | "undef": true, 12 | "unused": true, 13 | "boss": true, 14 | "eqnull": true, 15 | "node": true, 16 | "quotmark": "single", 17 | "indent": 4, 18 | "globals": { 19 | "describe": true, 20 | "it": true, 21 | "before": true, 22 | "beforeEach": true, 23 | "after": true, 24 | "afterEach": true, 25 | "document": true, 26 | "window": true, 27 | "navigator": true, 28 | "expect": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | 4 | language: node_js 5 | 6 | node_js: 7 | - '0.10' 8 | 9 | before_install: 10 | - sudo apt-get update 11 | - sudo apt-get install imagemagick graphicsmagick libcairo2-dev 12 | 13 | before_script: 14 | - "npm install -g bower http-server" 15 | - "cd test/site && bower install && cd ../../" 16 | - "curl -O http://selenium-release.storage.googleapis.com/2.43/selenium-server-standalone-2.43.1.jar" 17 | - "java -jar selenium-server-standalone-2.43.1.jar -host 127.0.0.1 -port 4444 2>/dev/null 1>/dev/null &" 18 | - "http-server -p 8080 &" 19 | - "sleep 10" 20 | - "if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then ./node_modules/.bin/istanbul i lib -o lib-cov && cp lib/getPageInfo.js lib-cov && cp lib/makeScreenshot.js lib-cov && cp lib/documentScreenshot.js lib-cov && cp lib/viewportScreenshot.js lib-cov && cp lib/startSession.js lib-cov && cp lib/setScreenWidth.js lib-cov; fi" 21 | 22 | script: "npm run-script travis" 23 | 24 | after_script: 25 | - "if [[ $WEBDRIVERCSS_COVERAGE == '1' ]]; then cat lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi" 26 | 27 | env: 28 | matrix: 29 | - MOCHA_REPORTERS: "spec" 30 | WEBDRIVERCSS_COVERAGE: "0" 31 | - MOCHA_REPORTERS: "mocha-istanbul" 32 | ISTANBUL_REPORTERS: "lcov" 33 | WEBDRIVERCSS_COVERAGE: "1" 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release History 2 | 3 | ## v1.1.10 (2015-12-19) 4 | * Adjust scrolling to better support sticky headers (#131) 5 | * Add option to tweak node-resemble-js image comparison (#121) 6 | * Screenshot height for different viewport widths (#120) 7 | * Replace falsy check of coordinates with proper numeric check (#117) 8 | 9 | ## v1.1.9 (2015-09-11) 10 | * make screenshots high dpi aware 11 | 12 | ## v1.1.8 (2015-09-05) 13 | * remove tarball after sync 14 | 15 | ## v1.1.7 (2015-08-20) 16 | * set scroll height of body before hiding overflow 17 | 18 | ## v1.1.6 (2015-08-06) 19 | * Save a new baseline image, if one doesn't already exist. - fixes #101 20 | 21 | ## v1.1.5 (2015-08-05) 22 | * improved screen capturing in IE - fixes #93 #91 23 | 24 | ## v1.1.4 (2015-07-09) 25 | * propagate arguments to result object - refs #88 26 | * Prevent webdrivercss from modifying an array passed to it - refs #77 27 | 28 | ## v1.1.3 (2015-05-12) 29 | * don't limit shot to body element 30 | * minor code and jslint fixes 31 | 32 | ## v1.1.2 (2015-05-05) 33 | * create random .tmp directories to run `documentScreenshot` in parallel - refs #71 34 | 35 | ## v1.1.1 (2015-05-02) 36 | * moved gm back to dependency list 37 | 38 | ## v1.1.0 (2015-04-30) 39 | * got rid of cairo and node-canvas dependency by replacing it with [node-resemble-js](https://www.npmjs.com/package/node-resemble-js) 40 | * minor bugfixes and documentation improvements 41 | 42 | ## v1.0.6 (2015-02-08) 43 | * Using fs-extra to recursively make directories that don't exist. (see #53) 44 | * Switch to node-resemble-js (fixes #49) 45 | 46 | ## v1.0.5 (2015-01-14) 47 | * Applitools integration: automatic test save (see #48) 48 | 49 | ## v1.0.4 (2015-01-14) 50 | * no changes 51 | 52 | ## v1.0.3 (2014-12-01) 53 | * reset takeScreenshot flag after webdrivercss command finised 54 | 55 | ## v1.0.2 (2014-12-01) 56 | * better support for IE<9 browser 57 | * remove scrollbars before taking screenshots 58 | 59 | ## v1.0.1 (2014-11-21) 60 | * fixed scope of variables 61 | * document screenshot little bit more mobile friendly 62 | 63 | ## v1.0.0 (2014-11-12) 64 | * introduced two commands (documentScreenshot, viewportScreenshot) 65 | * use documentScreenshot to always take screenshot of the whole website 66 | * implement support for [Applitools Eyes](https://applitools.com/) 67 | * better result propagation 68 | * changed filenames to *.baseline.png, *.regression.png and *.diff.png 69 | * reuse taken screenshots (different workflow as before) 70 | * minor IE improvements 71 | 72 | ## v0.3.1 (2014-10-24) 73 | * clear screenshot root properly 74 | 75 | ## v0.3.0 (2014-09-01) 76 | * make WebdriverCSS compatible with WebdriverIO 77 | 78 | ## v0.2.3 (2014-07-17) 79 | * x-browser/driver-compatibility improvements 80 | 81 | ## v0.2.2 (2014-07-15) 82 | * introduced `hide` option, remove local repository before download 83 | 84 | ## v0.2.1 (2014-07-13) 85 | * fixed scrollTo bug 86 | 87 | ## v0.2.0 (2014-07-12) 88 | * implemented shot synchronization with an external API 89 | 90 | ## v0.1.1 (2014-04-07) 91 | * convert screenWidth parameters into numbers 92 | 93 | ## v0.1.0 (2014-03-28) 94 | * first release 95 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Christian Bromann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebdriverCSS [![Version](http://img.shields.io/badge/version-v1.1.10-brightgreen.svg)](https://www.npmjs.org/package/webdrivercss) [![Build Status](https://travis-ci.org/webdriverio/webdrivercss.png?branch=master)](https://travis-ci.org/webdriverio/webdrivercss) [![Coverage Status](https://coveralls.io/repos/webdriverio/webdrivercss/badge.png?branch=master)](https://coveralls.io/r/webdriverio/webdrivercss?branch=master) [![Join the chat at https://gitter.im/webdriverio/webdrivercss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/webdriverio/webdrivercss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | ============ 3 | 4 | --- 5 | 6 | > **_Note:_ WebdriverCSS isn't yet compatible with WebdriverIO `v3.0` and is not longer maintained. ** If anyone wants to take ownershipt of this project I am happy to assign push permissions. 7 | 8 | --- 9 | 10 | __CSS regression testing in WebdriverIO__. This plugin is an automatic visual regression-testing 11 | tool for [WebdriverIO](http://webdriver.io). It was inspired by [James Cryer's](https://github.com/jamescryer) 12 | awesome project called [PhantomCSS](https://github.com/Huddle/PhantomCSS). After 13 | initialization it enhances a WebdriverIO instance with an additional command called 14 | `webdrivercss` and enables the possibility to save screenshots of specific parts of 15 | your application. 16 | 17 | #### Never lose track of unwanted CSS changes: 18 | 19 | ![alt text](http://webdriver.io/images/webdrivercss/hero.png "Logo Title Text 1") 20 | 21 | 22 | ## How does it work? 23 | 24 | 1. Define areas within your application that should always look the same 25 | 2. Use WebdriverIO and WebdriverCSS to write some E2E tests and take screenshots of these areas 26 | 3. Continue working on your application or website 27 | 4. After a while rerun the tests 28 | 5. If desired areas differ from previous taken screenshots an image diff gets generated and you get notified in your tests 29 | 30 | 31 | ### Example 32 | 33 | ```js 34 | var assert = require('assert'); 35 | 36 | // init WebdriverIO 37 | var client = require('webdriverio').remote({desiredCapabilities:{browserName: 'chrome'}}) 38 | // init WebdriverCSS 39 | require('webdrivercss').init(client); 40 | 41 | client 42 | .init() 43 | .url('http://example.com') 44 | .webdrivercss('startpage',[ 45 | { 46 | name: 'header', 47 | elem: '#header' 48 | }, { 49 | name: 'hero', 50 | elem: '//*[@id="hero"]/div[2]' 51 | } 52 | ], function(err, res) { 53 | assert.ifError(err); 54 | assert.ok(res.header[0].isWithinMisMatchTolerance); 55 | assert.ok(res.hero[0].isWithinMisMatchTolerance); 56 | }) 57 | .end(); 58 | ``` 59 | 60 | ## Install 61 | 62 | WebdriverCSS uses [GraphicsMagick](http://www.graphicsmagick.org/) for image processing. To install this 63 | package you'll need to have it preinstalled on your system. 64 | 65 | #### Mac OS X using [Homebrew](http://mxcl.github.io/homebrew/) 66 | ```sh 67 | $ brew install graphicsmagick 68 | ``` 69 | 70 | #### Ubuntu using apt-get 71 | ```sh 72 | $ sudo apt-get install graphicsmagick 73 | ``` 74 | 75 | #### Windows 76 | 77 | Download and install executables for [GraphicsMagick](http://www.graphicsmagick.org/download.html). 78 | Please make sure you install the right binaries desired for your system (32bit vs 64bit). 79 | 80 | After these dependencies are installed you can install WebdriverCSS via NPM as usual: 81 | 82 | ```sh 83 | $ npm install webdrivercss 84 | $ npm install webdriverio # if not already installed 85 | ``` 86 | 87 | ## Setup 88 | 89 | To use this plugin just call the `init` function and pass the desired WebdriverIO instance 90 | as parameter. Additionally you can define some options to configure the plugin. After that 91 | the `webdrivercss` command will be available only for this instance. 92 | 93 | * **screenshotRoot** `String` ( default: *./webdrivercss* )
94 | path where all screenshots get saved. 95 | 96 | * **failedComparisonsRoot** `String` ( default: *./webdrivercss/diff* )
97 | path where all screenshot diffs get saved. 98 | 99 | * **misMatchTolerance** `Number` ( default: *0.05* )
100 | number between 0 and 100 that defines the degree of mismatch to consider two images as 101 | identical, increasing this value will decrease test coverage. 102 | 103 | * **screenWidth** `Numbers[]` ( default: *[]* )
104 | if set, all screenshots will be taken in different screen widths (e.g. for responsive design tests) 105 | 106 | * **updateBaseline** `Boolean` ( default: *false* )
107 | updates baseline images if comparison keeps failing. 108 | 109 | 110 | ### Example 111 | 112 | ```js 113 | // create a WebdriverIO instance 114 | var client = require('webdriverio').remote({ 115 | desiredCapabilities: { 116 | browserName: 'phantomjs' 117 | } 118 | }).init(); 119 | 120 | // initialise WebdriverCSS for `client` instance 121 | require('webdrivercss').init(client, { 122 | // example options 123 | screenshotRoot: 'my-shots', 124 | failedComparisonsRoot: 'diffs', 125 | misMatchTolerance: 0.05, 126 | screenWidth: [320,480,640,1024] 127 | }); 128 | ``` 129 | 130 | ## Usage 131 | 132 | WebdriverCSS enhances an WebdriverIO instance with an command called `webdrivercss` 133 | 134 | `client.webdrivercss('some_id', [{options}], callback);` 135 | 136 | It provides options that will help you to define your areas exactly and exclude parts 137 | that are unrelevant for design (e.g. content). Additionally it allows you to include 138 | the responsive design in your regression tests easily. The following options are 139 | available: 140 | 141 | * **name** `String` (required)
142 | name of the captured element 143 | 144 | * **elem** `String`
145 | only capture a specific DOM element, you can use all kinds of different [WebdriverIO selector strategies](http://webdriver.io/guide/usage/selectors.html) here 146 | 147 | * **width** `Number`
148 | define a fixed width for your screenshot 149 | 150 | * **height** `Number`
151 | define a fixed height for your screenshot 152 | 153 | * **x** `Number`
154 | take screenshot at an exact xy postion (requires width/height option) 155 | 156 | * **y** `Number`
157 | take screenshot at an exact xy postion (requires width/height option) 158 | 159 | * **exclude** `String[]|Object[]`
160 | exclude frequently changing parts of your screenshot, you can either pass all kinds of different [WebdriverIO selector strategies](http://webdriver.io/guide/usage/selectors.html) 161 | that queries one or multiple elements or you can define x and y values which stretch a rectangle or polygon 162 | 163 | * **hide** `String[]`
164 | hides all elements queried by all kinds of different [WebdriverIO selector strategies](http://webdriver.io/guide/usage/selectors.html) (via `visibility: hidden`) 165 | 166 | * **remove** `String[]`
167 | removes all elements queried by all kinds of different [WebdriverIO selector strategies](http://webdriver.io/guide/usage/selectors.html) (via `display: none`) 168 | 169 | * **ignore** `String`
170 | can be used to ignore color differences or differences caused by antialising artifacts in the screenshot comparison 171 | 172 | The following paragraphs will give you a more detailed insight how to use these options properly. 173 | 174 | ### Let your test fail when screenshots differ 175 | 176 | When using this plugin you can decide how to handle design breaks. You can either just work 177 | with the captured screenshots or you could even break your integration test at this position. The 178 | following example shows how to handle design breaks within integration tests: 179 | 180 | ```js 181 | var assert = require('assert'); 182 | 183 | describe('my website should always look the same',function() { 184 | 185 | it('header should look the same',function(done) { 186 | client 187 | .url('http://www.example.org') 188 | .webdrivercss('header', { 189 | name: 'header', 190 | elem: '#header' 191 | }, function(err,res) { 192 | assert.ifError(err); 193 | 194 | // this will break the test if screenshot is not within the mismatch tolerance 195 | assert.ok(res.header[0].isWithinMisMatchTolerance); 196 | }) 197 | .call(done); 198 | }); 199 | 200 | // ... 201 | ``` 202 | 203 | The `res` variable will be an object containing details on the screenshots taken. It will have properties matching each element name, and the value of those properties will contain an array of screenshots at each resolution. 204 | 205 | For example, the `res` object for the code above would be: 206 | 207 | ```js 208 | { 209 | header: [ 210 | { 211 | baselinePath: 'webdrivercss/header.header.baseline.png', 212 | message: 'mismatch tolerance not exceeded (~0), baseline didn\'t change', 213 | misMatchPercentage: 0, 214 | isExactSameImage: true, 215 | isSameDimensions: true, 216 | isWithinMisMatchTolerance: true 217 | } 218 | ] 219 | } 220 | ``` 221 | 222 | ### [Applitools Eyes](http://applitools.com) Support 223 | 224 | ![Applitools Eyes](http://pravdam.biz/clientblogs/applitools2/applitools-new-logo.png) 225 | 226 | Applitools Eyes is a comprehensive automated UI validation solution with really smart image matching algorithms 227 | that are unique in this area. As a cloud service it makes your regression tests available everywhere and 228 | accessible to everyone in your team, and its automated maintenance features simplify baseline maintenance. 229 | 230 | In order to work with Applitools Eyes you need to sign up and obtain an API key. You can sign up for a 231 | free account [here](http://applitools.com/signup/). 232 | 233 | ### Applitools Eyes Example 234 | 235 | ```js 236 | var assert = require('assert'); 237 | 238 | // create a WebdriverIO instance 239 | var client = require('webdriverio').remote({ 240 | desiredCapabilities: { 241 | browserName: 'chrome' 242 | } 243 | }); 244 | 245 | // initialise WebdriverCSS for `client` instance 246 | require('webdrivercss').init(client, { 247 | key: '' 248 | }); 249 | 250 | client 251 | .init() 252 | .url('http://example.com') 253 | .webdrivercss('', { 254 | name: '', 255 | elem: '#someElement', 256 | // ... 257 | }, function(err, res) { 258 | assert.ifError(err); 259 | assert.equal(res.steps, res.strictMatches) 260 | }) 261 | .end(); 262 | ``` 263 | 264 | The following options might be interesting if you want to synchronize your taken images with 265 | an external API. Checkout the [webdrivercss-adminpanel](https://github.com/webdriverio/webdrivercss-adminpanel) 266 | for more information on that. 267 | 268 | * **api** `String` 269 | URL to API interface 270 | * **user** `String` 271 | user name (only necessary if API requires Basic Authentication or oAuth) 272 | * **key** `String` 273 | assigned user key (only necessary if API requires Basic Authentication or oAuth) 274 | 275 | 276 | ### Define specific areas 277 | 278 | The most powerful feature of WebdriverCSS is the possibility to define specific areas 279 | for your regression tests. When calling the command, WebdriverCSS will always take a screenshot of 280 | the whole website. After that it crops the image and creates a single copy for each element. 281 | If you want to capture multiple images on one page make sure you pass an array of options to 282 | the command. The screenshot capturing process can take a while depending on the document size 283 | of the website. Once you interact with the page by clicking on links, open layers or navigating 284 | to a new site you should call the `webdrivercss` command to take a new screenshot. 285 | 286 | To query elements you want to capture you are able to choose all kinds of different [WebdriverIO selector strategies](http://webdriver.io/guide/usage/selectors.html) or you can 287 | specify x/y coordinates to cover a more exact area. 288 | 289 | ```js 290 | client 291 | .url('http://github.com') 292 | .webdrivercss('githubform', { 293 | name: 'github-signup', 294 | elem: '#site-container > div.marketing-section.marketing-section-signup > div.container > form' 295 | }); 296 | ``` 297 | 298 | Will capture the following: 299 | 300 | ![alt text](http://webdriver.io/images/webdrivercss/githubform.png "Logo Title Text 1") 301 | 302 | **Tip:** do right click on the desired element, then click on `Inspect Element`, then hover 303 | over the desired element in DevTools, open the context menu and click on `Copy CSS Path` to 304 | get the exact CSS selector 305 | 306 | The following example uses xy coordinates to capture a more exact area. You should also 307 | pass a screenWidth option to make sure that your xy parameters map perfect on the desired area. 308 | 309 | ```js 310 | client 311 | .url('http://github.com') 312 | .webdrivercss('headerbar', { 313 | name: 'headerbar', 314 | x: 110, 315 | y: 15, 316 | width: 980, 317 | height: 34, 318 | screenWidth: [1200] 319 | }); 320 | ``` 321 | ![alt text](http://webdriver.io/images/webdrivercss/headerbar.png "Logo Title Text 1") 322 | 323 | 324 | ### Exclude specific areas 325 | 326 | Sometimes it is unavoidable that content gets captured and from time to time this content 327 | will change of course. This would break all tests. To prevent this you can 328 | determine areas, which will get covered in black and will not be considered anymore. Here is 329 | an example: 330 | 331 | ```js 332 | client 333 | .url('http://tumblr.com/themes') 334 | .webdrivercss('tumblrpage', { 335 | name: 'startpage', 336 | exclude: ['#theme_garden > div > section.carousel > div.carousel_slides', 337 | '//*[@id="theme_garden"]/div/section[3]', 338 | '//*[@id="theme_garden"]/div/section[4]'] 339 | screenWidth: [1200] 340 | }); 341 | ``` 342 | ![alt text](http://webdriver.io/images/webdrivercss/exclude.png "Logo Title Text 1") 343 | 344 | Instead of using a selector strategy you can also exclude areas by specifying xy values 345 | which form a rectangle. 346 | 347 | ```js 348 | client 349 | .url('http://tumblr.com/themes') 350 | .webdrivercss('tumblrpage', { 351 | name: 'startpage', 352 | exclude: [{ 353 | x0: 100, y0: 100, 354 | x1: 300, y1: 200 355 | }], 356 | screenWidth: [1200] 357 | }); 358 | ``` 359 | 360 | If your exclude object has more then two xy variables, it will try to form a polygon. This may be 361 | helpful if you like to exclude complex figures like: 362 | 363 | ```js 364 | client 365 | .url('http://tumblr.com/themes') 366 | .webdrivercss('polygon', { 367 | name: 'startpage', 368 | exclude: [{ 369 | x0: 120, y0: 725, 370 | x1: 120, y1: 600, 371 | x2: 290, y2: 490, 372 | x3: 290, y3: 255, 373 | x4: 925, y4: 255, 374 | x5: 925, y5: 490, 375 | x6: 1080,y6: 600, 376 | x7: 1080,y7: 725 377 | }], 378 | screenWidth: [1200] 379 | }); 380 | ``` 381 | ![alt text](http://webdriver.io/images/webdrivercss/exclude2.png "Logo Title Text 1") 382 | 383 | ### Tweak the image comparison 384 | 385 | If you experience problems with unstable comparison results you might want to try tweaking the algorithm. 386 | There are two options available: `colors` and `antialiasing`. `colors` might help you if you don't care about color differences on your page, while the `antialiasing` option can for example reduce unexpected differences on font or image edges: 387 | 388 | ```js 389 | client 390 | .url('http://tumblr.com/themes') 391 | .webdrivercss('tumblrpage', { 392 | name: 'startpage', 393 | ignore: 'antialiasing', 394 | screenWidth: [1200] 395 | }); 396 | ``` 397 | 398 | Note: This doesn't affect the taken screenshots, but only the comparison calculations. 399 | By setting this option you reduce the sensitivity of the comparison algorithm. Though it's unlikely this might cause layout changes to remain unnoticed. 400 | 401 | ### Keep an eye on mobile screen resolution 402 | 403 | It is of course also important to check your design in multiple screen resolutions. By 404 | using the `screenWidth` option WebdriverCSS automatically resizes the browser for you. 405 | By adding the screen width to the file name WebdriverCSS makes sure that only shots 406 | with same width will be compared. 407 | 408 | ```js 409 | client 410 | .url('http://stephencaver.com/') 411 | .webdrivercss('startpage', { 412 | name: 'header', 413 | elem: '#masthead', 414 | screenWidth: [320,640,960] 415 | }); 416 | ``` 417 | 418 | This will capture the following image at once: 419 | 420 | ![alt text](http://webdriver.io/images/webdrivercss/header.new.960px.png "Logo Title Text 1") 421 | 422 | **file name:** header.960px.png 423 | 424 | ![alt text](http://webdriver.io/images/webdrivercss/header.new.640px.png "Logo Title Text 1") 425 | 426 | **file name:** header.640px.png 427 | 428 | ![alt text](http://webdriver.io/images/webdrivercss/header.new.320px.png "Logo Title Text 1") 429 | 430 | **file name:** header.320px.png 431 | 432 | Note that if you have multiple tests running one after the other, it is important to change the first argument passed to the `webdrivercss()` command to be unique, as WebdriverCSS saves time by remembering the name of previously captured screenshots. 433 | 434 | ```js 435 | // Example using Mocha 436 | it('should check the first page',function(done) { 437 | client 438 | .init() 439 | .url('https://example.com') 440 | // Make this name unique. 441 | .webdrivercss('page1', [ 442 | { 443 | name: 'test', 444 | screenWidth: [320,480,640,1024] 445 | }, { 446 | name: 'test_two', 447 | screenWidth: [444,666] 448 | } 449 | ]) 450 | .end() 451 | .call(done); 452 | }); 453 | 454 | it('should check the second page',function(done) { 455 | client 456 | // ... 457 | // Make this name unique. 458 | .webdrivercss('page2', [ 459 | // .. 460 | ]) 461 | // ... 462 | ); 463 | 464 | ``` 465 | 466 | 467 | 468 | ### Synchronize your taken Images 469 | 470 | If you want to have your image repository available regardless where you run your tests, you can 471 | use an external API to store your shots. Therefor WebdriverCSS adds a `sync` function that downloads 472 | the repository as tarball and unzips it. After running your tests you can call this function again 473 | to zip the current state of your repository and upload it. Here is how this can look like: 474 | 475 | ```js 476 | // create a WebdriverIO instance 477 | var client = require('webdriverio').remote({ 478 | desiredCapabilities: { 479 | browserName: 'phantomjs' 480 | } 481 | }); 482 | 483 | // initialise WebdriverCSS for `client` instance 484 | require('webdrivercss').init(client, { 485 | screenshotRoot: 'myRegressionTests', 486 | 487 | // Provide the API route 488 | api: 'http://example.com/api/webdrivercss' 489 | }); 490 | 491 | client 492 | .init() 493 | .sync() // downloads last uploaded tarball from http://example.com/api/webdrivercss/myRegressionTests.tar.gz 494 | .url('http://example.com') 495 | 496 | // do your regression tests 497 | // ... 498 | 499 | .sync() // zips your screenshot root and uploads it to http://example.com/api/webdrivercss via POST method 500 | .end(); 501 | ``` 502 | 503 | This allows you to run your regression tests with the same taken shots again and again, no matter where 504 | your tests are executed. It also makes distributed testing possible. Regressions tests can be done not only 505 | by you but everyone else who has access to the API. 506 | 507 | #### API Requirements 508 | 509 | To implement such API you have to provide two routes for synchronization: 510 | 511 | * [GET] /some/route/:file 512 | Should response the uploaded tarball (for example: /some/root/myProject.tar.gz) 513 | Content-Type: `application/octet-stream` 514 | * [POST] /some/route 515 | Request contains zipped tarball that needs to be stored on the filesystem 516 | 517 | If you don't want to implement this by yourself, there is already such an application prepared, checkout 518 | the [webdriverio/webdrivercss-adminpanel](https://github.com/webdriverio/webdrivercss-adminpanel) project. 519 | It provides even a web interface for before/after comparison and stuff like this. 520 | 521 | ## Contributing 522 | Please fork, add specs, and send pull requests! In lieu of a formal styleguide, take care to 523 | maintain the existing coding style. 524 | 525 | Default driver instance used for testing is [PhantomJS](https://github.com/ariya/phantomjs) (v1.9.8), so you need to either have 526 | it installed, or change the `desiredCapabilities` in the `bootstrap.js` 527 | file under the `test` folder to your preferred driver (e.g., Firefox). 528 | 529 | You also need a web server to serve the "site" files and have the root folder set to "webdrivercss". We use the 530 | [http-server package](https://www.npmjs.org/package/http-server). To use this package, run these commands: 531 | 532 | ``` 533 | /path/to/webdrivercss-repo/ $ npm install -g http-server 534 | /path/to/webdrivercss-repo/ $ http-server -p 8080 535 | ``` 536 | 537 | You can validate the site is loading correctly by visiting `http://localhost:8080/test/site/index.html` in a browser. 538 | 539 | You'll also need a local selenium server running. You can install and start one via the following commands in a separate terminal: 540 | 541 | ``` 542 | npm install -g selenium-standalone 543 | selenium-standalone install 544 | selenium-standalone start 545 | ``` 546 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore the output of these examples 2 | visual/failed 3 | visual/reference 4 | 5 | # if you want to save ONLY the baseline images and not the full screenshots, 6 | # use a rule like this: 7 | !visual/reference/*baseline* 8 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # WebDriverCSS Examples 2 | 3 | These examples are provided only for convenience, and might fall out of date from time to time. Also, please note that you shouldn't be attempting to run the examples from this directory. Rather, copy them to a new directory that has a proper `package.json` and other supporting files. 4 | -------------------------------------------------------------------------------- /examples/webdrivercss.browserstack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NPM packages 4 | var webdriverio = require('webdriverio'); 5 | var webdrivercss = require('webdrivercss'); 6 | 7 | // All options go here, to allow easier boilerplating. 8 | var options = { 9 | browser: { 10 | 'browserstack.debug': 'true', 11 | 'browserstack.local': 'true', 12 | os: 'Windows', 13 | os_version: '7', 14 | browser: 'ie', 15 | browser_version: '9.0' 16 | }, 17 | test: { 18 | title: 'Body_win7-ie9', 19 | name: 'body', 20 | url: 'http://localhost:3000/my/test/url', // this needs to be a real URL 21 | selector: 'body', 22 | }, 23 | webdrivercss: { 24 | screenshotRoot: 'visual/reference', 25 | failedComparisonsRoot: 'visual/failed', 26 | misMatchTolerance: 0.05, 27 | screenWidth: [1024] 28 | } 29 | }; 30 | 31 | // Get your key here: https://www.browserstack.com/accounts/automate 32 | // 33 | // Script assumes your BrowserStack creds are listed in the JSON file. 34 | // Convenient if you want to avoid storing keys in VCS. If storing in 35 | // VCS is ok, just assign an object literal to config: 36 | // 37 | // var config = { 38 | // "browserstack": { 39 | // "user": "MY_USER", 40 | // "key": "MY_KEY" 41 | // } 42 | // } 43 | var config = require('./browserstack.json'); 44 | 45 | // Configure webdriverio 46 | var client = webdriverio.remote({ 47 | desiredCapabilities: options.browser, 48 | host: 'hub.browserstack.com', 49 | port: 80, 50 | user: config.browserstack.user, 51 | key: config.browserstack.key 52 | }).init(); 53 | 54 | // Initialize webdrivercss 55 | webdrivercss.init(client, options.webdrivercss); 56 | 57 | // Run the test 58 | client 59 | .url(options.test.url) 60 | .webdrivercss(options.test.title, { 61 | name: options.test.name, 62 | elem: options.test.selector 63 | }, function(err, res) { 64 | console.log(err); 65 | console.log(res); 66 | }) 67 | .end(); 68 | -------------------------------------------------------------------------------- /examples/webdrivercss.browserstack.with.mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Node deps 4 | var assert = require('assert'); 5 | 6 | // NPM packages 7 | var webdriverio = require('webdriverio'); 8 | var webdrivercss = require('webdrivercss'); 9 | 10 | // All configuration goes here, to allow easier boilerplating. 11 | var options = { 12 | browser: { 13 | 'browserstack.debug': 'true', 14 | 'browserstack.local': 'true', 15 | os: 'Windows', 16 | os_version: '7', 17 | browser: 'ie', 18 | browser_version: '9.0' 19 | }, 20 | test: { 21 | title: 'Body_win7-ie9', 22 | name: 'body', 23 | url: 'http://localhost:3000/my/test/url', // this needs to be a real URL 24 | selector: 'body', 25 | }, 26 | webdrivercss: { 27 | screenshotRoot: 'visual/reference', 28 | failedComparisonsRoot: 'visual/failed', 29 | misMatchTolerance: 0.05, 30 | screenWidth: [1024] 31 | } 32 | }; 33 | 34 | // Get your key here: https://www.browserstack.com/accounts/automate 35 | // 36 | // Script assumes your BrowserStack creds are listed in JSON somewhere in your 37 | // system. Convenient if you want to avoid storing keys in VCS. If storing in 38 | // VCS is ok, just assign an object literal to config: 39 | // 40 | // var config = { 41 | // "browserstack": { 42 | // "user": "MY_USER", 43 | // "key": "MY_KEY" 44 | // } 45 | // } 46 | var config = require('./browserstack.json'); 47 | 48 | // Configure webdriverio 49 | var client = webdriverio.remote({ 50 | desiredCapabilities: options.browser, 51 | host: 'hub.browserstack.com', 52 | port: 80, 53 | user: config.browserstack.user, 54 | key: config.browserstack.key 55 | }); 56 | 57 | // Run the test 58 | describe('Win7 / IE9: My Component @ 1024', function () { 59 | this.timeout(600000); 60 | 61 | // If multiple tests are run by mocha, use its setup function to initialize 62 | // webdriverio and webdrivercss. Otherwise, BrowserStack connections might 63 | // timeout while you wait for the first few tests to run. 64 | before(function(){ 65 | // Initialize webdriverio 66 | client.init(); 67 | // Initialize webdrivercss 68 | webdrivercss.init(client, options.webdrivercss); 69 | }); 70 | 71 | it('should look the same', function (done) { 72 | client 73 | .url(options.test.url) 74 | .webdrivercss(options.test.title, { 75 | name: options.test.name, 76 | elem: options.test.selector 77 | }, function(err, res) { 78 | assert.strictEqual(res[options.test.name][0].isWithinMisMatchTolerance, true); 79 | }) 80 | .end() 81 | .call(done); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | pkgFile: 'package.json', 4 | clean: ['build'], 5 | babel: { 6 | options: { 7 | sourceMap: false, 8 | plugins: ['transform-async-to-generator'] 9 | }, 10 | dist: { 11 | files: [{ 12 | expand: true, 13 | cwd: './lib', 14 | src: ['*.js'], 15 | dest: 'build', 16 | ext: '.js' 17 | }] 18 | } 19 | }, 20 | watch: { 21 | dist: { 22 | files: ['./lib/*.js'], 23 | tasks: ['babel:dist'] 24 | } 25 | }, 26 | eslint: { 27 | options: { 28 | parser: 'babel-eslint' 29 | }, 30 | target: ['lib/*.js'] 31 | }, 32 | contributors: { 33 | options: { 34 | commitMessage: 'update contributors' 35 | } 36 | }, 37 | bump: { 38 | options: { 39 | commitMessage: 'v%VERSION%', 40 | pushTo: 'upstream' 41 | } 42 | }, 43 | webdriver: { 44 | options: { 45 | logLevel: 'command', 46 | waitforTimeout: 12345, 47 | framework: 'mocha', 48 | coloredLogs: true 49 | }, 50 | testTargetConfigFile: { 51 | configFile: './test/wdio.conf.js', 52 | foo: 'bar' 53 | } 54 | } 55 | }) 56 | 57 | require('load-grunt-tasks')(grunt) 58 | grunt.loadTasks('build') 59 | grunt.registerTask('default', ['build']) 60 | grunt.registerTask('build', 'Build grunt-webdriver', function () { 61 | grunt.task.run([ 62 | // 'eslint', 63 | 'clean', 64 | 'babel' 65 | ]) 66 | }) 67 | grunt.registerTask('release', 'Bump and tag version', function (type) { 68 | grunt.task.run([ 69 | 'build', 70 | 'contributors', 71 | 'bump:' + (type || 'patch') 72 | ]) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebdriverCSS 3 | * Regression testing tool for WebdriverIO 4 | * 5 | * @author Christian Bromann 6 | * @license Licensed under the MIT license. 7 | */ 8 | 9 | module.exports = process.env.WEBDRIVERCSS_COVERAGE === '1' ? require('./build-cov/webdrivercss.js') : require('./build/webdrivercss.js'); 10 | -------------------------------------------------------------------------------- /lib/asyncCallback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * run workflow again or execute callback function 5 | */ 6 | 7 | var workflow = require('./workflow.js'), 8 | endSession = require('./endSession.js'); 9 | 10 | module.exports = function(err) { 11 | 12 | var that = this; 13 | 14 | /** 15 | * if error occured don't do another shot (if multiple screen width are set) 16 | */ 17 | /*istanbul ignore next*/ 18 | if(err) { 19 | return this.cb(err); 20 | } 21 | 22 | /** 23 | * on multiple screenWidth or multiple page elements 24 | * repeat workflow 25 | */ 26 | if(this.screenWidth && this.screenWidth.length) { 27 | 28 | /** 29 | * if multiple screen widths are given 30 | * start workflow all over again with same parameter 31 | */ 32 | this.queuedShots[0].screenWidth = this.screenWidth; 33 | return workflow.call(this.self, this.pagename, this.queuedShots, this.cb); 34 | 35 | } else if (this.queuedShots.length > 1) { 36 | 37 | /** 38 | * if multiple page modules are given 39 | */ 40 | return endSession.call(this, function() { 41 | that.queuedShots.shift(); 42 | return workflow.call(that.self, that.pagename, that.queuedShots, that.cb); 43 | }); 44 | 45 | } 46 | 47 | /** 48 | * finish command 49 | */ 50 | return endSession.call(this, function(err) { 51 | that.self.takeScreenshot = undefined; 52 | that.cb(err, that.self.resultObject); 53 | that.self.resultObject = {}; 54 | }); 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /lib/compareImages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * compare images 5 | */ 6 | 7 | var resemble = require('node-resemble-js'); 8 | 9 | module.exports = function() { 10 | 11 | /** 12 | * need to find done function because gm doesn't have node like callbacks (err,res) 13 | */ 14 | var done = arguments[arguments.length - 1]; 15 | 16 | /** 17 | * if there is no need for image comparison or no images gets saved on fs, just continue 18 | */ 19 | if(!this.isComparable || !this.self.saveImages) { 20 | return done(); 21 | } 22 | 23 | /** 24 | * compare images 25 | */ 26 | var diff = resemble(this.baselinePath).compareTo(this.regressionPath); 27 | 28 | /** 29 | * map 'ignore' configuration to resemble options 30 | */ 31 | var ignore = this.currentArgs.ignore || ""; 32 | if (ignore.indexOf("color") === 0) { 33 | diff.ignoreColors(); 34 | } else if (ignore.indexOf("antialias") === 0) { 35 | diff.ignoreAntialiasing(); 36 | } 37 | 38 | /** 39 | * execute the comparison 40 | */ 41 | diff.onComplete(done.bind(null,null)); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/cropImage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * crop image according to user arguments and its position on screen and save it 5 | */ 6 | 7 | var gm = require('gm'), 8 | async = require('async'), 9 | request = require('request'), 10 | exclude = require('./exclude.js'); 11 | 12 | module.exports = function(res, done) { 13 | 14 | var that = this, 15 | excludeRect = res.excludeRect, 16 | shot = gm(this.screenshot).quality(100), 17 | cropDim; 18 | 19 | var x = parseInt(this.currentArgs.x, 10); 20 | var y = parseInt(this.currentArgs.y, 10); 21 | var width = parseInt(this.currentArgs.width, 10); 22 | var height = parseInt(this.currentArgs.height, 10); 23 | 24 | if (!isNaN(x) && !isNaN(y) && !isNaN(width) && !isNaN(height)) { 25 | 26 | /** 27 | * crop image with given arguments 28 | */ 29 | cropDim = { 30 | x: x - res.scrollPos.x, 31 | y: y - res.scrollPos.y, 32 | width: width, 33 | height: height 34 | }; 35 | 36 | exclude(shot, excludeRect); 37 | shot.crop(cropDim.width, cropDim.height, cropDim.x, cropDim.y); 38 | 39 | } else if (res && res.elemBounding) { 40 | 41 | /** 42 | * or use boundary of specific CSS element 43 | */ 44 | cropDim = { 45 | x: res.elemBounding.left + (res.elemBounding.width / 2), 46 | y: res.elemBounding.top + (res.elemBounding.height / 2), 47 | width: isNaN(width) ? res.elemBounding.width : width, 48 | height: isNaN(height) ? res.elemBounding.height : height 49 | }; 50 | 51 | exclude(shot, excludeRect); 52 | shot.crop(cropDim.width, cropDim.height, cropDim.x - (cropDim.width / 2), cropDim.y - (cropDim.height / 2)); 53 | 54 | } else { 55 | exclude(shot, excludeRect); 56 | } 57 | 58 | async.waterfall([ 59 | /** 60 | * save image to fs 61 | */ 62 | function(cb) { 63 | if(!that.self.saveImages) { 64 | return cb(); 65 | } 66 | 67 | return shot.write(that.filename || that.baselinePath, cb); 68 | }, 69 | /** 70 | * generate image buffer 71 | */ 72 | function() { 73 | var cb = arguments[arguments.length - 1]; 74 | return shot.toBuffer('PNG', cb); 75 | }, 76 | /** 77 | * upload image to applitools 78 | */ 79 | function(buffer) { 80 | var cb = arguments[arguments.length - 1]; 81 | if (!that.self.usesApplitools) { 82 | return cb(); 83 | } 84 | request({ 85 | qs: {apiKey: that.applitools.apiKey}, 86 | url: that.self.host + '/api/sessions/running/' + that.self.sessionId, 87 | method: 'POST', 88 | headers: that.self.headers, 89 | timeout: that.self.reqTimeout, 90 | json: { 91 | 'appOutput': { 92 | 'title': res.title, 93 | 'screenshot64': new Buffer(buffer).toString('base64') 94 | }, 95 | 'tag': that.currentArgs.tag || '', 96 | 'ignoreMismatch': that.currentArgs.ignoreMismatch || false 97 | } 98 | }, cb); 99 | } 100 | ], done); 101 | 102 | }; 103 | -------------------------------------------------------------------------------- /lib/documentScreenshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Save a screenshot as a base64 encoded PNG with the current state of the browser. 4 | * 5 | * 6 | :saveScreenshot.js 7 | client 8 | // set browser window size 9 | .windowHandleSize({width: 500, height: 500}) 10 | .saveScreenshot('viewport.png') // make screenshot of current viewport (500x500px) 11 | .saveScreenshot('wholeScreen.png', true) // makes screenshot of whole document (1280x1342px) 12 | .end(); 13 | * 14 | * 15 | * @param {String} fileName path of generated image (relative to the execution directory) 16 | * @param {Boolean=} totalScreen if true (default value) it takes a screenshot of whole website, otherwise only of current viewport 17 | * 18 | * @uses protocol/execute, utility/scroll, protocol/screenshot 19 | * @type utility 20 | * 21 | */ 22 | 23 | import fs from 'fs' 24 | import gm from 'gm' 25 | import path from 'path' 26 | import rimraf from 'rimraf' 27 | 28 | import generateUUID from './helpers/generateUUID' 29 | import scrollFn from './scripts/scroll' 30 | import getPageInfo from './scripts/getPageInfo' 31 | 32 | const CAPS_TAKING_FULLSIZE_SHOTS = ['firefox'] 33 | 34 | export async function documentScreenshot (fileName) { 35 | let ErrorHandler = this.instance.ErrorHandler 36 | 37 | /*! 38 | * parameter check 39 | */ 40 | if (typeof fileName !== 'string') { 41 | throw new ErrorHandler.CommandError(`filename from type string is require, got ${fileName}`) 42 | } 43 | 44 | let cropImages = [] 45 | let currentXPos = 0 46 | let currentYPos = 0 47 | let screenshot = null 48 | 49 | /*! 50 | * create tmp directory to cache viewport shots 51 | */ 52 | let uuid = generateUUID() 53 | let tmpDir = path.join(__dirname, '..', '.tmp-' + uuid) 54 | // Todo: promisify this 55 | if (!fs.existsSync(tmpDir)) { 56 | fs.mkdirSync(tmpDir, '0755') 57 | } 58 | 59 | /*! 60 | * prepare page scan 61 | */ 62 | const pageInfo = (await this.execute(getPageInfo)).value 63 | 64 | /** 65 | * no need to stitch viewports together if screenshot is from the whole website 66 | */ 67 | if (CAPS_TAKING_FULLSIZE_SHOTS.indexOf(this.browserName) === -1) { 68 | const screenshot = await this.saveScreenshot(fileName) 69 | return screenshot 70 | } 71 | 72 | /*! 73 | * run scan 74 | */ 75 | while (currentXPos < (pageInfo.documentWidth / pageInfo.screenWidth)) { 76 | /*! 77 | * take screenshot of viewport 78 | */ 79 | let shot = await this.screenshot() 80 | 81 | /*! 82 | * cache viewport image into tmp dir 83 | */ 84 | let file = tmpDir + '/' + currentXPos + '-' + currentYPos + '.png' 85 | let gmImage = gm(new Buffer(shot.value, 'base64')) 86 | 87 | if (pageInfo.devicePixelRatio > 1) { 88 | var percent = 100 / pageInfo.devicePixelRatio 89 | gmImage.resize(percent, percent, '%') 90 | } 91 | 92 | gmImage.crop(pageInfo.screenWidth, pageInfo.screenHeight, 0, 0) 93 | 94 | if (!cropImages[currentXPos]) { 95 | cropImages[currentXPos] = [] 96 | } 97 | 98 | cropImages[currentXPos][currentYPos] = file 99 | 100 | currentYPos++ 101 | if (currentYPos > Math.floor(pageInfo.documentHeight / pageInfo.screenHeight)) { 102 | currentYPos = 0 103 | currentXPos++ 104 | } 105 | 106 | await new Promise((resolve) => gmImage.write(file, resolve)) 107 | 108 | /*! 109 | * scroll to next area 110 | */ 111 | await this.execute(scrollFn, 112 | currentXPos * pageInfo.screenWidth, 113 | currentYPos * pageInfo.screenHeight 114 | ) 115 | 116 | /** 117 | * have a small break to allow browser to render 118 | */ 119 | await this.pause(50) 120 | } 121 | 122 | /*! 123 | * concats all shots 124 | */ 125 | var subImg = 0 126 | 127 | for (let snippet in cropImages) { 128 | var col = gm(snippet.shift()) 129 | col.append.apply(col, snippet) 130 | 131 | if (!screenshot) { 132 | screenshot = col 133 | await new Promise((resolve) => col.write(fileName, resolve)) 134 | } else { 135 | await new Promise((resolve) => 136 | col.write(tmpDir + '/' + (++subImg) + '.png', () => 137 | gm(fileName).append(tmpDir + '/' + subImg + '.png', true).write(fileName, resolve) 138 | )) 139 | } 140 | } 141 | 142 | /*! 143 | * crop screenshot regarding page size 144 | */ 145 | await new Promise((resolve) => 146 | gm(fileName).crop(pageInfo.documentWidth, pageInfo.documentHeight, 0, 0).write(fileName, resolve)) 147 | 148 | /*! 149 | * remove tmp dir 150 | * Todo: promisify 151 | */ 152 | rimraf.sync(tmpDir) 153 | 154 | /*! 155 | * scroll back to start position 156 | */ 157 | await this.execute(scrollFn, 0, 0) 158 | } 159 | -------------------------------------------------------------------------------- /lib/endSession.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'), 4 | merge = require('deepmerge'), 5 | request = require('request'); 6 | 7 | module.exports = function(done) { 8 | 9 | var that = this; 10 | 11 | async.waterfall([ 12 | /** 13 | * if screenwidth was set, get back to old resolution 14 | */ 15 | function(cb) { 16 | if (!that.self.defaultScreenDimension) { 17 | return cb(); 18 | } 19 | 20 | that.instance.windowHandleSize({ 21 | width: that.self.defaultScreenDimension.width, 22 | height: that.self.defaultScreenDimension.height 23 | }, cb); 24 | }, 25 | /** 26 | * end session when using applitools 27 | */ 28 | function() { 29 | var cb = arguments[arguments.length - 1]; 30 | 31 | if(!that.self.usesApplitools) { 32 | return cb(); 33 | } 34 | 35 | // Whether or not we should automatically save this test as baseline. 36 | var updateBaseline = (that.self.isNew && that.applitools.saveNewTests) || 37 | (!that.self.isNew && that.applitools.saveFailedTests); 38 | 39 | return request({ 40 | qs: {apiKey: that.applitools.apiKey, updateBaseline: updateBaseline}, 41 | url: that.self.host + '/api/sessions/running/' + that.self.sessionId, 42 | method: 'DELETE', 43 | headers: that.self.headers, 44 | timeout: that.self.reqTimeout 45 | }, cb); 46 | }, 47 | /** 48 | * clear session, store result 49 | */ 50 | function(res, body) { 51 | var cb = arguments[arguments.length - 1]; 52 | 53 | if(body) { 54 | that.self.resultObject[that.currentArgs.name] = merge({ 55 | id: that.self.sessionId, 56 | url: that.self.url 57 | }, JSON.parse(body)); 58 | that.self.url = undefined; 59 | that.self.sessionId = undefined; 60 | that.self.isNew = undefined; 61 | } 62 | return cb(); 63 | } 64 | 65 | ], function(err) { 66 | return done(err); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /lib/exclude.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * exclude parts within page by drawing black rectangle 5 | */ 6 | 7 | module.exports = function(shot, excludeRect) { 8 | 9 | excludeRect.forEach(function(rect) { 10 | 11 | if(Object.keys(rect).length > 4) { 12 | 13 | var points = []; 14 | for(var i = 0; i < Object.keys(rect).length / 2; i++) { 15 | points.push([rect['x'+i] , rect['y'+i]]); 16 | } 17 | 18 | shot.drawPolygon(points); 19 | 20 | } else { 21 | 22 | shot.drawRectangle(rect.x0, rect.y0, rect.x1, rect.y1); 23 | 24 | } 25 | 26 | }); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /lib/getPageInfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * get page informations 5 | * IMPORTANT: all of this code gets executed on browser side, so you won't have 6 | * access to node specific interfaces at all 7 | */ 8 | var async = require('async'), 9 | merge = require('deepmerge'); 10 | 11 | /** 12 | * little helper function to check against argument values 13 | * @param {Object} variable some variable 14 | * @return {Boolean} is true if typeof variable is number 15 | */ 16 | function isNumber(variable) { 17 | return typeof variable === 'number'; 18 | } 19 | 20 | module.exports = function(done) { 21 | var that = this, 22 | response = { 23 | excludeRect: [], 24 | scrollPos: {x: 0, y:0}, 25 | }, 26 | excludeRect = [], 27 | element = that.currentArgs.elem; 28 | 29 | async.waterfall([ 30 | /** 31 | * get page information 32 | */ 33 | function(cb) { 34 | that.instance.execute(function() { 35 | /** 36 | * get current scroll position 37 | * @return {Object} x and y coordinates of current scroll position 38 | */ 39 | var getScrollPosition = function() { 40 | var x = 0, 41 | y = 0; 42 | 43 | if (typeof window.pageYOffset === 'number') { 44 | 45 | /* Netscape compliant */ 46 | y = window.pageYOffset; 47 | x = window.pageXOffset; 48 | 49 | } else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) { 50 | 51 | /* DOM compliant */ 52 | y = document.body.scrollTop; 53 | x = document.body.scrollLeft; 54 | 55 | } else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) { 56 | 57 | /* IE6 standards compliant mode */ 58 | y = document.documentElement.scrollTop; 59 | x = document.documentElement.scrollLeft; 60 | 61 | } 62 | 63 | return { 64 | x: x, 65 | y: y 66 | }; 67 | }; 68 | 69 | return { 70 | title: document.title, 71 | scrollPos: getScrollPosition(), 72 | screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), 73 | screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) 74 | }; 75 | 76 | }, cb); 77 | }, 78 | 79 | /** 80 | * get element information 81 | */ 82 | function(res, cb) { 83 | response = merge(response, res.value); 84 | 85 | if(!element) { 86 | return cb(null, {}, {}); 87 | } 88 | 89 | /** 90 | * needs to get defined that verbose to make it working in IE driver 91 | */ 92 | that.instance.selectorExecute(element, function(elem) { 93 | var boundingRect = elem[0].getBoundingClientRect(); 94 | return { 95 | elemBounding: { 96 | width: boundingRect.width ? boundingRect.width : boundingRect.right - boundingRect.left, 97 | height: boundingRect.height ? boundingRect.height : boundingRect.bottom - boundingRect.top, 98 | top: boundingRect.top, 99 | right: boundingRect.right, 100 | bottom: boundingRect.bottom, 101 | left: boundingRect.left 102 | } 103 | }; 104 | }, cb); 105 | }, 106 | 107 | /** 108 | * get information about exclude elements 109 | */ 110 | function(res, responses, done) { 111 | response = merge(response, res); 112 | 113 | /** 114 | * concatenate exclude elements to one dimensional array 115 | * excludeElements = elements queried by specific selector strategy (typeof string) 116 | * excludeCoords = x & y coords to exclude custom areas 117 | */ 118 | var excludeElements = []; 119 | 120 | if (!that.currentArgs.exclude) { 121 | return done(null, []); 122 | } else if (!(that.currentArgs.exclude instanceof Array)) { 123 | that.currentArgs.exclude = [that.currentArgs.exclude]; 124 | } 125 | 126 | that.currentArgs.exclude.forEach(function(excludeElement) { 127 | if (typeof excludeElement === 'string') { 128 | excludeElements.push(excludeElement); 129 | } else { 130 | /** 131 | * excludeCoords are a set of x,y rectangle 132 | * then just check if the first 4 coords are numbers (minumum to span a rectangle) 133 | */ 134 | if (isNumber(excludeElement.x0) && isNumber(excludeElement.x1) && isNumber(excludeElement.y0) && isNumber(excludeElement.y1)) { 135 | response.excludeRect.push(excludeElement); 136 | } 137 | } 138 | }); 139 | 140 | if(excludeElements.length === 0) { 141 | return done(null, []); 142 | } 143 | 144 | that.instance.selectorExecute(excludeElements, function() { 145 | 146 | /** 147 | * excludeElements are elements queried by specific selenium strategy 148 | */ 149 | var excludeElements = Array.prototype.slice.call(arguments), 150 | excludeRect = []; 151 | 152 | excludeElements.forEach(function(elements) { 153 | 154 | if(!elements) { 155 | return; 156 | } 157 | 158 | elements.forEach(function(elem) { 159 | var elemRect = elem.getBoundingClientRect(); 160 | excludeRect.push({ 161 | x0: elemRect.left, 162 | y0: elemRect.top, 163 | x1: elemRect.right, 164 | y1: elemRect.bottom 165 | }); 166 | }); 167 | }); 168 | 169 | return excludeRect; 170 | 171 | }, done); 172 | } 173 | ], function(err, excludeElements) { 174 | 175 | if(excludeElements && excludeElements.length) { 176 | response.excludeRect = excludeRect.concat(excludeElements); 177 | } 178 | 179 | done(err, response); 180 | }); 181 | }; 182 | -------------------------------------------------------------------------------- /lib/helpers/generateUUID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @return {string} A V4 UUID. 3 | * @private 4 | */ 5 | export function generateUUID () { 6 | let d = new Date().getTime() 7 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 8 | const r = (d + Math.random() * 16) % 16 | 0 9 | d = Math.floor(d / 16) 10 | return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /lib/logWarning.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * prints warning message within WebdriverIO instance 5 | * @param {String} id error type 6 | */ 7 | 8 | /*istanbul ignore next*/ 9 | module.exports = function(id) { 10 | var prefix = '\x1b[1;32mWebdriverCSS\x1b[0m\t'; 11 | 12 | switch(id) { 13 | case 'NoElementFound': 14 | this.logger.log(prefix + 'Couldn\'t find element on page'); 15 | this.logger.log(prefix + 'taking screenshot of whole website'); break; 16 | 17 | case 'ArgumentsMailformed': 18 | this.logger.log(prefix + 'No element or bounding is given'); 19 | this.logger.log(prefix + 'taking screenshot of whole website'); break; 20 | 21 | case 'DimensionWarning': 22 | this.logger.log(prefix + 'new image snapshot has a different dimension'); break; 23 | 24 | default: 25 | this.logger.log(prefix + 'Unknown warning'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/makeScreenshot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * make screenshot via [GET] /session/:sessionId/screenshot 5 | */ 6 | var modifyElements = function(elements, style, value) { 7 | if(elements.length === 0) { 8 | return; 9 | } 10 | 11 | this.instance.selectorExecute(elements, function() { 12 | var args = Array.prototype.slice.call(arguments).filter(function(n){ return !!n; }), 13 | style = args[args.length - 2], 14 | value = args[args.length - 1]; 15 | 16 | args.splice(-2); 17 | for(var i = 0; i < args.length; ++i) { 18 | for(var j = 0; j < args[i].length; ++j) { 19 | args[i][j].style[style] = value; 20 | } 21 | } 22 | 23 | }, style, value); 24 | }; 25 | 26 | module.exports = function(done) { 27 | 28 | /** 29 | * take actual screenshot in given screensize just once 30 | */ 31 | if(this.self.takeScreenshot === false) { 32 | return done(); 33 | } 34 | 35 | this.self.takeScreenshot = false; 36 | 37 | /** 38 | * gather all elements to hide 39 | */ 40 | var hiddenElements = [], 41 | removeElements = []; 42 | this.queuedShots.forEach(function(args) { 43 | if(typeof args.hide === 'string') { 44 | hiddenElements.push(args.hide); 45 | } 46 | if(args.hide instanceof Array) { 47 | hiddenElements = hiddenElements.concat(args.hide); 48 | } 49 | if(typeof args.remove === 'string') { 50 | removeElements.push(args.remove); 51 | } 52 | if(args.remove instanceof Array) { 53 | removeElements = removeElements.concat(args.remove); 54 | } 55 | }); 56 | 57 | /** 58 | * hide / remove elements 59 | */ 60 | modifyElements.call(this, hiddenElements, 'visibility', 'hidden'); 61 | modifyElements.call(this, removeElements, 'display', 'none'); 62 | 63 | /** 64 | * take 100ms pause to give browser time for rendering 65 | */ 66 | this.instance.pause(100).saveDocumentScreenshot(this.screenshot, done); 67 | 68 | /** 69 | * make hidden elements visible again 70 | */ 71 | modifyElements.call(this, hiddenElements, 'visibility', ''); 72 | modifyElements.call(this, removeElements, 'display', ''); 73 | 74 | }; 75 | -------------------------------------------------------------------------------- /lib/renameFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var glob = require('glob'), 4 | fs = require('fs'); 5 | 6 | module.exports = function() { 7 | var done = arguments[arguments.length - 1]; 8 | 9 | glob('{' + this.regressionPath + ',' + this.baselinePath + '}', {}, function(err,files) { 10 | 11 | /** 12 | * if no files were found continue 13 | */ 14 | if(files.length === 0) { 15 | return done(); 16 | } 17 | 18 | this.isComparable = true; 19 | this.filename = this.regressionPath; 20 | 21 | /** 22 | * rename existing files 23 | */ 24 | if(files.length === 2 && this.updateBaseline && !this.self.usesApplitools) { 25 | return fs.rename(this.regressionPath, this.baselinePath, done); 26 | } else { 27 | return done(); 28 | } 29 | 30 | }.bind(this)); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/saveImageDiff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | async = require('async'), 5 | logWarning = require('./logWarning.js'); 6 | 7 | module.exports = function(imageDiff,done) { 8 | 9 | var that = this, 10 | misMatchTolerance = parseFloat(imageDiff.misMatchPercentage,10); 11 | 12 | if(typeof imageDiff === 'function') { 13 | this.self.resultObject[this.currentArgs.name].push({ 14 | baselinePath: this.baselinePath, 15 | message: 'first image of module "' + this.currentArgs.name + '" from page "' + this.pagename + '" successfully taken', 16 | misMatchPercentage: 0, 17 | isExactSameImage: true, 18 | isSameDimensions: true, 19 | isWithinMisMatchTolerance: true, 20 | properties: this.currentArgs 21 | }); 22 | 23 | return imageDiff(); 24 | } 25 | 26 | /** 27 | * if set misMatchTolerance is smaller then compared misMatchTolerance 28 | * make image diff 29 | */ 30 | if(this.misMatchTolerance < misMatchTolerance) { 31 | 32 | /*istanbul ignore next*/ 33 | if(!imageDiff.isSameDimensions) { 34 | logWarning.call(this.instance, 'DimensionWarning'); 35 | } 36 | 37 | this.self.resultObject[this.currentArgs.name].push({ 38 | baselinePath: this.baselinePath, 39 | regressionPath: this.regressionPath, 40 | diffPath: this.diffPath, 41 | message: 'mismatch tolerance exceeded (+' + (misMatchTolerance - this.misMatchTolerance) + '), image-diff created', 42 | misMatchPercentage: misMatchTolerance, 43 | isExactSameImage: false, 44 | isSameDimensions: imageDiff.isSameDimensions, 45 | isWithinMisMatchTolerance: false, 46 | properties: this.currentArgs 47 | }); 48 | 49 | imageDiff.getDiffImage().pack() 50 | .on('end', done.bind(null, null, this.resultObject)) 51 | .pipe(fs.createWriteStream(this.diffPath)); 52 | 53 | } else { 54 | 55 | /** 56 | * otherwise delete diff 57 | */ 58 | 59 | async.waterfall([ 60 | /** 61 | * check if diff shot exists 62 | */ 63 | function(done) { 64 | fs.exists(that.diffPath,done.bind(null,null)); 65 | }, 66 | /** 67 | * remove diff if yes 68 | */ 69 | function(exists,done) { 70 | if(exists) { 71 | fs.unlink(that.diffPath,done); 72 | } else { 73 | done(); 74 | } 75 | }, 76 | /** 77 | * Save a new baseline image, if one doesn't already exist. 78 | * 79 | * If one does exist, we delete the temporary regression. 80 | */ 81 | function(done) { 82 | fs.exists(that.baselinePath, function(exists) { 83 | return !!exists ? fs.unlink(that.regressionPath, done) : fs.rename(that.regressionPath, that.baselinePath, done); 84 | }); 85 | } 86 | ], function(err) { 87 | 88 | /** 89 | * return result object to WebdriverIO instance 90 | */ 91 | that.self.resultObject[that.currentArgs.name].push({ 92 | baselinePath: that.baselinePath, 93 | message: 'mismatch tolerance not exceeded (~' + misMatchTolerance + '), baseline didn\'t change', 94 | misMatchPercentage: misMatchTolerance, 95 | isExactSameImage: misMatchTolerance === 0, 96 | isSameDimensions: imageDiff.isSameDimensions, 97 | isWithinMisMatchTolerance: true 98 | }); 99 | 100 | done(err); 101 | 102 | }); 103 | 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /lib/scripts/getPageInfo.js: -------------------------------------------------------------------------------- 1 | export function getPageInfo () { 2 | var body = document.body 3 | var html = document.documentElement 4 | 5 | /** 6 | * remove scrollbars 7 | * reset height in case we're changing viewports 8 | */ 9 | body.style.height = 'auto' 10 | body.style.height = html.scrollHeight + 'px' 11 | body.style.overflow = 'hidden' 12 | 13 | /** 14 | * scroll back to start scanning 15 | */ 16 | window.scrollTo(0, 0) 17 | 18 | /** 19 | * get viewport width/height and total width/height 20 | */ 21 | var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) 22 | 23 | return { 24 | screenWidth: Math.max(html.clientWidth, window.innerWidth || 0), 25 | screenHeight: Math.max(html.clientHeight, window.innerHeight || 0), 26 | documentWidth: html.scrollWidth, 27 | documentHeight: height, 28 | devicePixelRatio: window.devicePixelRatio 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/scripts/getScreenDimension.js: -------------------------------------------------------------------------------- 1 | export function getScreenDimension () { 2 | return { 3 | screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), 4 | screenHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/scripts/getScrollingPosition.js: -------------------------------------------------------------------------------- 1 | export function getScrollingPosition () { 2 | var position = [0, 0] 3 | 4 | if (typeof window.pageYOffset !== 'undefined') { 5 | return [window.pageXOffset, window.pageYOffset] 6 | } else if (typeof document.documentElement.scrollTop !== 'undefined' && document.documentElement.scrollTop > 0) { 7 | return [document.documentElement.scrollLeft, document.documentElement.scrollTop] 8 | } else if (typeof document.body.scrollTop !== 'undefined') { 9 | return [document.body.scrollLeft, document.body.scrollTop] 10 | } 11 | 12 | return position 13 | } 14 | -------------------------------------------------------------------------------- /lib/scripts/scroll.js: -------------------------------------------------------------------------------- 1 | export function scroll (w, h) { 2 | /** 3 | * IE8 or older 4 | */ 5 | if (document.all && !document.addEventListener) { 6 | /** 7 | * this still might not work 8 | * seems that IE8 scroll back to 0,0 before taking screenshots 9 | */ 10 | document.body.style.marginTop = '-' + h + 'px' 11 | document.body.style.marginLeft = '-' + w + 'px' 12 | return 13 | } 14 | 15 | document.documentElement.style.webkitTransform = 'translate(-' + w + 'px, -' + h + 'px)' 16 | document.documentElement.style.mozTransform = 'translate(-' + w + 'px, -' + h + 'px)' 17 | document.documentElement.style.msTransform = 'translate(-' + w + 'px, -' + h + 'px)' 18 | document.documentElement.style.oTransform = 'translate(-' + w + 'px, -' + h + 'px)' 19 | document.documentElement.style.transform = 'translate(-' + w + 'px, -' + h + 'px)' 20 | }; 21 | -------------------------------------------------------------------------------- /lib/setScreenWidth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * if multiple screen width are given resize browser dimension 5 | */ 6 | 7 | var async = require('async'), 8 | takenScreenSizes = {}; 9 | 10 | module.exports = function(done) { 11 | 12 | var that = this; 13 | this.newScreenSize = {}; 14 | 15 | async.waterfall([ 16 | /** 17 | * get current browser resolution to change back to it 18 | * after all shots were taken (only if a screenWidth is set) 19 | */ 20 | function(cb) { 21 | if(!that.self.defaultScreenDimension && that.screenWidth && that.screenWidth.length) { 22 | that.instance.windowHandleSize(function(err,res) { 23 | that.self.defaultScreenDimension = res.value; 24 | cb(); 25 | }); 26 | } else { 27 | cb(); 28 | } 29 | }, 30 | function(cb) { 31 | 32 | if(!that.screenWidth || that.screenWidth.length === 0) { 33 | 34 | /** 35 | * if no screenWidth option was set just continue 36 | */ 37 | return cb(); 38 | 39 | } 40 | 41 | that.newScreenSize.width = parseInt(that.screenWidth.shift(), 10); 42 | that.newScreenSize.height = parseInt(that.self.defaultScreenDimension.height, 10); 43 | 44 | that.self.takeScreenshot = false; 45 | if(!takenScreenSizes[that.pagename] || takenScreenSizes[that.pagename].indexOf(that.newScreenSize.width) < 0) { 46 | /** 47 | * set flag to retake screenshot 48 | */ 49 | that.self.takeScreenshot = true; 50 | 51 | /** 52 | * cache already taken screenshot / screenWidth combinations 53 | */ 54 | if(!takenScreenSizes[that.pagename]) { 55 | takenScreenSizes[that.pagename] = [that.newScreenSize.width]; 56 | } else { 57 | takenScreenSizes[that.pagename].push(that.newScreenSize.width); 58 | } 59 | } 60 | 61 | /** 62 | * resize browser resolution 63 | */ 64 | that.instance.call(function() { 65 | 66 | /** 67 | * if shot will be taken in a specific screenWidth, rename file and append screen width 68 | * value in filename 69 | */ 70 | that.baselinePath = that.baselinePath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); 71 | that.regressionPath = that.regressionPath.replace(/\.(baseline|regression|diff)\.png/,'.' + that.newScreenSize.width + 'px.$1.png'); 72 | that.diffPath = that.diffPath.replace(/\.(baseline|regression|diff)\.png/, '.' + that.newScreenSize.width + 'px.$1.png'); 73 | that.screenshot = that.screenshot.replace(/\.png/, '.' + that.newScreenSize.width + 'px.png'); 74 | that.filename = that.baselinePath; 75 | 76 | that.instance.setViewportSize({width: that.newScreenSize.width, height: that.newScreenSize.height}) 77 | .pause(100) 78 | .call(cb); 79 | 80 | }); 81 | } 82 | ], done); 83 | }; 84 | -------------------------------------------------------------------------------- /lib/startSession.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pkg = require('../package.json'), 4 | request = require('request'), 5 | async = require('async'), 6 | WebdriverIO = require('webdriverio'); 7 | 8 | module.exports = function() { 9 | 10 | var that = this, 11 | done = arguments[arguments.length - 1]; 12 | 13 | /** 14 | * skip when not using applitools 15 | */ 16 | if(!this.self.usesApplitools || this.self.sessionId) { 17 | return done(); 18 | } 19 | 20 | async.waterfall([ 21 | 22 | /** 23 | * get meta information of current session 24 | */ 25 | function(cb) { 26 | that.instance.execute(function() { 27 | return { 28 | useragent: navigator.userAgent, 29 | screenWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), 30 | documentHeight: document.documentElement.scrollHeight 31 | }; 32 | }, function(err, res) { 33 | that.useragent = res.value.useragent; 34 | that.displaySize = { 35 | width: that.screenWidth && that.screenWidth.length ? that.screenWidth[0] : res.value.screenWidth, 36 | height: res.value.documentHeight 37 | }; 38 | 39 | return cb(); 40 | }); 41 | }, 42 | 43 | /** 44 | * initialise applitools session 45 | */ 46 | function(cb) { 47 | request({ 48 | url: that.self.host + '/api/sessions/running', 49 | qs: {apiKey: that.applitools.apiKey}, 50 | method: 'POST', 51 | json: { 52 | 'startInfo': { 53 | 'envName': that.applitools.baselineName, 54 | 'appIdOrName': that.applitools.appName, 55 | 'scenarioIdOrName': that.currentArgs.name, 56 | 'batchInfo': { 57 | 'id': that.applitools.batchId, 58 | 'name': that.pagename, 59 | 'startedAt': new Date().toISOString() 60 | }, 61 | 'environment': { 62 | 'displaySize': that.displaySize, 63 | 'inferred': 'useragent:' + that.useragent 64 | }, 65 | 'matchLevel': 'Strict', 66 | 'agentId': pkg.name + '/' + pkg.version 67 | } 68 | }, 69 | headers: that.self.headers, 70 | timeout: that.self.reqTimeout 71 | }, cb); 72 | 73 | } 74 | ], function(err, res, body) { 75 | 76 | if (err || res.statusCode !== 200 && res.statusCode !== 201) { 77 | return done(new WebdriverIO.ErrorHandler.CommandError('Couldn\'t start applitools session')); 78 | } 79 | 80 | that.self.sessionId = body.id; 81 | that.self.url = body.url; 82 | that.self.isNew = res.statusCode === 201; 83 | return done(); 84 | 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /lib/syncImages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs-extra'), 4 | tar = require('tar'), 5 | zlib = require('zlib'), 6 | targz = require('tar.gz'), 7 | rimraf = require('rimraf'), 8 | request = require('request'); 9 | 10 | /** 11 | * sync down 12 | * downloads tarball from API and unzip it 13 | * 14 | * @param {Function} done callback to be called after sync finishes 15 | */ 16 | var syncDown = function(done) { 17 | 18 | var args = { 19 | url: this.api + (this.api.substr(-1) !== '/' ? '/' : '') + this.screenshotRoot + '.tar.gz', 20 | headers: { 'accept-encoding': 'gzip,deflate' }, 21 | }; 22 | 23 | if(typeof this.user === 'string' && typeof this.key === 'string') { 24 | args.auth = { 25 | user: this.user, 26 | pass: this.key 27 | }; 28 | } 29 | 30 | var r = request.get(args), 31 | self = this; 32 | 33 | r.on('error', done); 34 | r.on('response', function(resp) { 35 | 36 | /*! 37 | * no error if repository doesn't exists 38 | */ 39 | /*istanbul ignore if*/ 40 | if(resp.statusCode === 404) { 41 | return done(); 42 | } 43 | 44 | /*istanbul ignore next*/ 45 | if(resp.statusCode !== 200 || resp.headers['content-type'] !== 'application/octet-stream') { 46 | return done(new Error('unexpected statusCode (' + resp.statusCode + ' != 200) or content-type (' + resp.headers['content-type'] + ' != application/octet-stream)')); 47 | } 48 | 49 | /** 50 | * check if repository directory already exists and 51 | * clear it if yes 52 | */ 53 | if(fs.existsSync(self.screenshotRoot)) { 54 | rimraf.sync(self.screenshotRoot); 55 | fs.mkdirsSync(self.screenshotRoot, '0755', true); 56 | } 57 | 58 | resp.pipe(zlib.Gunzip()).pipe(tar.Extract({ path: '.' })).on('end', done); 59 | 60 | }); 61 | }; 62 | 63 | /** 64 | * sync up 65 | * zips image repository and uploads it to API 66 | * 67 | * @param {Function} done callback to be called after tarball was uploaded 68 | */ 69 | var syncUp = function(done) { 70 | var screenshotRoot = this.screenshotRoot, 71 | args = { url: this.api }, 72 | tarballPath = screenshotRoot + '.tar.gz'; 73 | 74 | if(typeof this.user === 'string' && typeof this.key === 'string') { 75 | args.auth = { 76 | user: this.user, 77 | pass: this.key 78 | }; 79 | } 80 | 81 | new targz().compress(screenshotRoot, tarballPath, function(err){ 82 | 83 | /*istanbul ignore if*/ 84 | if(err) { 85 | return done(new Error(err)); 86 | } 87 | 88 | var r = request.post(args, function () { 89 | rimraf.sync(tarballPath); 90 | done(); 91 | }); 92 | 93 | var form = r.form(); 94 | form.append('gz', fs.createReadStream(tarballPath)); 95 | 96 | }); 97 | }; 98 | 99 | /** 100 | * sync command 101 | * decides according to `needToSync` flag when to sync down and when to sync up 102 | * 103 | * @param {Function} done callback to be called after syncing 104 | * @return {Object} WebdriverCSS instance 105 | */ 106 | module.exports = function(done) { 107 | 108 | if(!this.api) { 109 | return done(new Error('No sync options specified! Please provide an api path and user/key (optional).')); 110 | } 111 | 112 | var sync = this.needToSync ? syncUp : syncDown; 113 | this.needToSync = false; 114 | 115 | sync.call(this, function(err, httpResponse, body) { 116 | 117 | /*istanbul ignore next*/ 118 | if(err || (httpResponse && httpResponse.statusCode !== 200)) { 119 | return done(new Error(err || body)); 120 | } 121 | 122 | done(); 123 | 124 | }); 125 | return this; 126 | }; 127 | -------------------------------------------------------------------------------- /lib/viewportScreenshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * takes screenshot of the current viewport 3 | * 4 | * @param {String} filename path of file to be saved 5 | */ 6 | 7 | import gm from 'gm' 8 | import ErrorHandler from 'webdriverio/lib/utils/ErrorHandler' 9 | 10 | import getScrollingPosition from './scripts/getScrollingPosition' 11 | import getScreenDimension from './scripts/getScreenDimension' 12 | 13 | export default async function viewportScreenshot (fileName) { 14 | /*! 15 | * parameter check 16 | */ 17 | if (typeof fileName !== 'string') { 18 | throw new ErrorHandler.CommandError('number or type of arguments don\'t agree with saveScreenshot command') 19 | } 20 | 21 | /*! 22 | * get page information like 23 | * - scroll position 24 | * - viewport width/height and total width/height 25 | */ 26 | let scrollPosition = await this.execute(getScrollingPosition) 27 | let screenDimension = await this.execute(getScreenDimension) 28 | 29 | /** 30 | * take screenshot 31 | */ 32 | let screenshot = await this.screenshot() 33 | 34 | /** 35 | * crop image 36 | */ 37 | return await new Promise((resolve, reject) => { 38 | gm(new Buffer(screenshot.value, 'base64')).crop( 39 | // width 40 | screenDimension.value.screenWidth, 41 | // height 42 | screenDimension.value.screenHeight, 43 | // top 44 | scrollPosition.value[0], 45 | // left 46 | scrollPosition.value[1] 47 | ).write(fileName, (err) => { 48 | if (err) { 49 | return reject(err) 50 | } 51 | 52 | return resolve() 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /lib/webdrivercss.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import merge from 'deepmerge' 3 | 4 | import workflow from './workflow.js' 5 | import viewportScreenshot from './viewportScreenshot.js' 6 | import documentScreenshot from './documentScreenshot.js' 7 | import syncImages from './syncImages' 8 | 9 | const DEFAULT_PROPERTIES = { 10 | screenshotRoot: 'webdrivercss', 11 | failedComparisonsRoot: 'webdrivercss/diff', 12 | misMatchTolerance: 0.05, 13 | screenWidth: [], 14 | warning: [], 15 | resultObject: {}, 16 | updateBaseline: false 17 | } 18 | 19 | /** 20 | * WebdriverCSS 21 | * initialise plugin 22 | */ 23 | class WebdriverCSS { 24 | constructor (webdriverInstance, options) { 25 | if (!webdriverInstance) { 26 | throw new Error('A WebdriverIO instance is needed to initialise WebdriverCSS') 27 | } 28 | 29 | this.instance = webdriverInstance 30 | this.options = merge(DEFAULT_PROPERTIES, options || {}) 31 | 32 | /** 33 | * create directory if it doesn't already exist 34 | */ 35 | this.createDirectory(this.options.screenshotRoot) 36 | this.createDirectory(this.options.failedComparisonsRoot) 37 | 38 | /** 39 | * add WebdriverCSS command to WebdriverIO instance 40 | */ 41 | this.instance.addCommand('saveViewportScreenshot', viewportScreenshot.bind(this)) 42 | this.instance.addCommand('saveDocumentScreenshot', documentScreenshot.bind(this)) 43 | this.instance.addCommand('webdrivercss', workflow.bind(this)) 44 | this.instance.addCommand('sync', syncImages.bind(this)) 45 | } 46 | 47 | createDirectory (path) { 48 | if (fs.existsSync(path)) { 49 | return 50 | } 51 | 52 | fs.mkdirsSync(path, '0755', true) 53 | } 54 | } 55 | 56 | /** 57 | * expose WebdriverCSS 58 | */ 59 | module.exports.init = (webdriverInstance, options) => new WebdriverCSS(webdriverInstance, options) 60 | -------------------------------------------------------------------------------- /lib/workflow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * run regression test 5 | */ 6 | 7 | var async = require('async'); 8 | 9 | module.exports = function(pagename, args) { 10 | 11 | /*! 12 | * make sure that callback contains chainit callback 13 | */ 14 | var cb = arguments[arguments.length - 1]; 15 | 16 | this.needToSync = true; 17 | 18 | /*istanbul ignore next*/ 19 | if (typeof args === 'function') { 20 | args = {}; 21 | } 22 | 23 | if (!(args instanceof Array)) { 24 | args = [args]; 25 | } 26 | 27 | /** 28 | * parameter type check 29 | */ 30 | /*istanbul ignore next*/ 31 | if (typeof pagename === 'function') { 32 | throw new Error('A pagename is required'); 33 | } 34 | /*istanbul ignore next*/ 35 | if (typeof args[0].name !== 'string') { 36 | throw new Error('You need to specify a name for your visual regression component'); 37 | } 38 | 39 | var queuedShots = JSON.parse(JSON.stringify(args)), 40 | currentArgs = queuedShots[0]; 41 | 42 | var context = { 43 | self: this, 44 | 45 | /** 46 | * default attributes 47 | */ 48 | misMatchTolerance: this.misMatchTolerance, 49 | screenshotRoot: this.screenshotRoot, 50 | failedComparisonsRoot: this.failedComparisonsRoot, 51 | 52 | instance: this.instance, 53 | pagename: pagename, 54 | applitools: { 55 | apiKey: this.applitools.apiKey, 56 | appName: pagename, 57 | baselineName: this.applitools.baselineName, 58 | saveNewTests: this.applitools.saveNewTests, 59 | saveFailedTests: this.applitools.saveFailedTests, 60 | batchId: this.applitools.batchId // Group all sessions for this instance together. 61 | }, 62 | currentArgs: currentArgs, 63 | queuedShots: queuedShots, 64 | baselinePath: this.screenshotRoot + '/' + pagename + '.' + currentArgs.name + '.baseline.png', 65 | regressionPath: this.screenshotRoot + '/' + pagename + '.' + currentArgs.name + '.regression.png', 66 | diffPath: this.failedComparisonsRoot + '/' + pagename + '.' + currentArgs.name + '.diff.png', 67 | screenshot: this.screenshotRoot + '/' + pagename + '.png', 68 | isComparable: false, 69 | warnings: [], 70 | newScreenSize: 0, 71 | pageInfo: null, 72 | updateBaseline: (typeof currentArgs.updateBaseline === 'boolean') ? currentArgs.updateBaseline : this.updateBaseline, 73 | screenWidth: currentArgs.screenWidth || [].concat(this.screenWidth), // create a copy of the origin default screenWidth 74 | cb: cb 75 | }; 76 | 77 | /** 78 | * initiate result object 79 | */ 80 | if(!this.resultObject[currentArgs.name]) { 81 | this.resultObject[currentArgs.name] = []; 82 | } 83 | 84 | async.waterfall([ 85 | 86 | /** 87 | * initialize session 88 | */ 89 | require('./startSession.js').bind(context), 90 | 91 | /** 92 | * if multiple screen width are given resize browser dimension 93 | */ 94 | require('./setScreenWidth.js').bind(context), 95 | 96 | /** 97 | * make screenshot via [GET] /session/:sessionId/screenshot 98 | */ 99 | require('./makeScreenshot.js').bind(context), 100 | 101 | /** 102 | * check if files with id already exists 103 | */ 104 | require('./renameFiles.js').bind(context), 105 | 106 | /** 107 | * get page informations 108 | */ 109 | require('./getPageInfo.js').bind(context), 110 | 111 | /** 112 | * crop image according to user arguments and its position on screen and save it 113 | */ 114 | require('./cropImage.js').bind(context), 115 | 116 | /** 117 | * compare images 118 | */ 119 | require('./compareImages.js').bind(context), 120 | 121 | /** 122 | * save image diff 123 | */ 124 | require('./saveImageDiff.js').bind(context) 125 | ], 126 | /** 127 | * run workflow again or execute callback function 128 | */ 129 | require('./asyncCallback.js').bind(context) 130 | 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdrivercss", 3 | "version": "1.1.10", 4 | "description": "Regression testing tool for WebdriverJS", 5 | "author": "Christian Bromann ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "./node_modules/.bin/mocha", 10 | "travis": "./node_modules/.bin/mocha -R $MOCHA_REPORTERS", 11 | "prepublish": "npm prune" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/webdriverio/webdrivercss.git" 16 | }, 17 | "dependencies": { 18 | "async": "^0.9.0", 19 | "deepmerge": "^0.2.7", 20 | "fs-extra": "^0.18.2", 21 | "glob": "^5.0.5", 22 | "gm": "^1.17.0", 23 | "node-resemble-js": "0.0.4", 24 | "request": "^2.55.0", 25 | "rimraf": "^2.3.2", 26 | "tar": "^2.1.0", 27 | "tar.gz": "^1.0.1" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.6.5", 31 | "babel-core": "^6.7.2", 32 | "babel-eslint": "^6.0.0", 33 | "babel-preset-es2015": "^6.6.0", 34 | "babel-register": "^6.7.2", 35 | "babel-plugin-transform-async-to-generator": "^6.8.0", 36 | "chai": "^2.3.0", 37 | "coveralls": "~2.11.2", 38 | "eslint": "^2.5.1", 39 | "eslint-config-standard": "^5.1.0", 40 | "eslint-plugin-promise": "^1.1.0", 41 | "eslint-plugin-standard": "^1.3.2", 42 | "grunt": "^0.4.5", 43 | "grunt-babel": "^6.0.0", 44 | "grunt-bump": "^0.7.0", 45 | "grunt-cli": "^1.2.0", 46 | "grunt-contrib-clean": "^1.0.0", 47 | "grunt-contrib-watch": "^1.0.0", 48 | "grunt-eslint": "^18.0.0", 49 | "grunt-npm": "0.0.2", 50 | "istanbul": "^0.3.13", 51 | "load-grunt-tasks": "^3.4.1", 52 | "mocha": "^2.2.4", 53 | "mocha-istanbul": "^0.2.0", 54 | "nock": "^1.7.1", 55 | "wdio-mocha-framework": "^0.2.12", 56 | "webdriverio": "^4.0.9" 57 | }, 58 | "keywords": [ 59 | "webdriverjs", 60 | "webdriverio", 61 | "webdriver", 62 | "phantomcss", 63 | "regression", 64 | "test", 65 | "testing", 66 | "css", 67 | "responsive", 68 | "design" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * require dependencies 3 | */ 4 | WebdriverIO = require('webdriverio'); 5 | WebdriverCSS = require('../index.js'); 6 | fs = require('fs-extra'); 7 | gm = require('gm'); 8 | glob = require('glob'); 9 | async = require('async'); 10 | should = require('chai').should(); 11 | expect = require('chai').expect; 12 | capabilities = {logLevel: 'silent',desiredCapabilities:{browserName: 'phantomjs'}}; 13 | testurl = 'http://localhost:8080/test/site/index.html'; 14 | testurlTwo = 'http://localhost:8080/test/site/two.html'; 15 | testurlThree = 'http://localhost:8080/test/site/three.html'; 16 | testurlFour = 'http://localhost:8080/test/site/four.html'; 17 | 18 | /** 19 | * set some fix test variables 20 | */ 21 | screenshotRootDefault = 'webdrivercss'; 22 | failedComparisonsRootDefault = 'webdrivercss/diff'; 23 | screenshotRootCustom = '__screenshotRoot__'; 24 | failedComparisonsRootCustom = '__failedComparisonsRoot__'; 25 | 26 | afterHook = function(done) { 27 | 28 | var browser = this.browser; 29 | 30 | /** 31 | * close browser and clean up created directories 32 | */ 33 | async.parallel([ 34 | function(done) { browser.end(done); }, 35 | function(done) { fs.remove(failedComparisonsRootDefault,done); }, 36 | function(done) { fs.remove(screenshotRootDefault,done); }, 37 | function(done) { fs.remove(failedComparisonsRootCustom,done); }, 38 | function(done) { fs.remove(screenshotRootCustom,done); } 39 | ], done); 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /test/fixtures/comparisonTest.diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/comparisonTest.diff.png -------------------------------------------------------------------------------- /test/fixtures/excludeElem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/excludeElem.png -------------------------------------------------------------------------------- /test/fixtures/hideElem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/hideElem.png -------------------------------------------------------------------------------- /test/fixtures/notWithinViewport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/notWithinViewport.png -------------------------------------------------------------------------------- /test/fixtures/testAtSpecificPosition.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/testAtSpecificPosition.current.png -------------------------------------------------------------------------------- /test/fixtures/testWithGivenElement.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/testWithGivenElement.current.png -------------------------------------------------------------------------------- /test/fixtures/testWithGivenElementAndWidthHeight.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/testWithGivenElementAndWidthHeight.current.png -------------------------------------------------------------------------------- /test/fixtures/testWithWidthHeightParameter.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/testWithWidthHeightParameter.current.png -------------------------------------------------------------------------------- /test/fixtures/testWithoutParameter.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/testWithoutParameter.current.png -------------------------------------------------------------------------------- /test/fixtures/timeoutTest.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/timeoutTest.current.png -------------------------------------------------------------------------------- /test/fixtures/timeoutTestWorking.current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/timeoutTestWorking.current.png -------------------------------------------------------------------------------- /test/fixtures/webdrivercss.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/fixtures/webdrivercss.tar.gz -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | test/bootstrap.js 2 | test/spec/*.js 3 | 4 | --timeout 1000000, 5 | --reporter spec 6 | -------------------------------------------------------------------------------- /test/site/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "components" 3 | } -------------------------------------------------------------------------------- /test/site/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WebdriverIO Testpage", 3 | "version": "0.0.1", 4 | "homepage": "http://webdriver.io", 5 | "authors": [ 6 | "christian-bromann " 7 | ], 8 | "main": "index.html", 9 | "license": "MIT", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "jquery-ui": "~1.10.3", 20 | "jquery-hammerjs": "~1.1.3", 21 | "jquery": "~2.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/site/css/main.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | padding: 0!important; 3 | margin: 0!important; 4 | background: url("data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABkAAD/4QPAaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjAtYzA2MCA2MS4xMzQ3NzcsIDIwMTAvMDIvMTItMTc6MzI6MDAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcFJpZ2h0cz0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3JpZ2h0cy8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBSaWdodHM6TWFya2VkPSJGYWxzZSIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjQxMjI1QzI1MTYyMDY4MTE4MDgzRTRDQTMyODg4MkVEIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkEzQkM2RUI4MDc4RjExRTNBMzExQ0E1REMxMUQxQkMyIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkEzQkM2RUI3MDc4RjExRTNBMzExQ0E1REMxMUQxQkMyIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MjgxOEY1MjEzMjA2ODExODA4M0FFMDk1RUQwMzdGMCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0MTIyNUMyNTE2MjA2ODExODA4M0U0Q0EzMjg4ODJFRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQECAgICAgICAgICAgMDAwMDAwMDAwMBAQEBAQEBAgEBAgICAQICAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA//AABEIADMAMwMBEQACEQEDEQH/xABrAAADAQEBAAAAAAAAAAAAAAAAAgMBBAoBAQAAAAAAAAAAAAAAAAAAAAAQAAIDAAEDAgQEBQUAAAAAAAECERIDEwAhIjEyQiMzBEFiFDRRYVKCQ3GRY4MkEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD3KWBOnY7sHYVOqK4gSmilF0UtkqQxKdz4yYAIMFEXbiYLmwD28bksFSpU/cdygUg0mG909wu2bPnRjmBZllWyV+bR00IzoSFR9UBlh5EwFEqOgki5KdXFXdnVrugwzooOduTVX76WMQT5jvYAdA+rZroyCSvpskHQrm6tm2OjZqSCNQYUGJUkt3EBItomDacmfmQLumxIzYGRmHQKRibMsS/Y9z3ABp19stHLy/Vx/c/VinJHFb8f7rT0DZjFQxD6uxU5pPcvne2xVXzJ1cBfRWdZcyFBIAMGceWbvCgjVUTUNkqtoua+dT9uzIWiAzBD/oSE1w3GKwfFn2YFEZBm6hkXRRLOshZAK9l/ECOgUuUpoaZqylgcteTuLAgPdcUfNJABkG57EwoBhNlHmoA+1sUgSVUZuNA7wRtofQBvEL2iOgm6BRcr45le9sgjMPrHJdCg4l2WKAd+Tv6gdB0V2ieNfoz7W+venHTkrxx8Xt+KLd+gxqfcMGUflR7aPlo+IZqLoTlVlT2EsV8iZBkEMbIMg8KOR2dyvygVUsmmrIwKXQwDCsvYGI6CrSuSKEUHwzaFChRQgkO1ddSAxKk9hBJMjuHIEz1DVI5C2nKyKcl4cjpfcDWM03ZtTBaKhT3B7kHyfJXnIIxYEvmj7/MhgLBvOrvJEGRUFSZJ6Ac6IdGZx8YWXZOfMIqBc0Oiglq+sljf+oLIH6r7r14xPN+pvXT6/pTk5IrTwr/Z69+g3yEOy50e5jPECW4FI2XaoTR1sxqGNR2HYeIK2GmWdGOjkoUYyGAZ0B3Lhc9TmihXA+Kyn8CYB+AR8vJzxWepbMBs2Z68opKZ6MwEBipQFvGZITV9EzBaLHjPHkygoILZNxq6mNG0ZSvYEMAsdh0DjbTMsmbFmZ9iCCywmiIitk2jDhZbEBz4z6EnxIaq8gOfCE5QC326irKQlg5cHMiUUqSGEdzXygATpNv1WvvmbmOGvFWLR+s/Dj9s9/59ApV08tMlXUwzZl1TmRXZipQFgc0ZJaRae7HsZBlVS+ebaQzzQhlqc1D4NllY0Y93KhZZQfWTPQSDE6IubJbgYmPlsiAQFXQzGiWmHcsDmTMkDoLL6l+R2ZiVXQaBFGjszfMyD5gtq6fykiGqfIAZKyqQiJnGjqoz4VadNMzmA2o0AGnYmJgkRB7AFL8WhANsHg6PUycbgVdzFrEEQAAZH8XHQXqPTv749vh+si1ptF5/xzS/aZ8egUa305CWric2axEtnqXUaaaHUwtCZ7ha/gQbAAOui5jXVwuiu06irMDzFGc7WyGaq1kE1AB/gD0BnosM2bKmVSHyVuJQod2RyXzYnV6lCUkdomIqEf0+RVNCUe+i+h0AB1SzEOxYg2QHvPjYiegENYay2FjSBTV6UPDdEXjdZDohjxg1MdBV3ySY0+30b3TSuwAszqrs7QRmxHorFRAJJB6DaaenLpHPb9sK88zyfua2t/12/N0EsornHL7PurXvEfNjlt3r6TXv9T83QZ9vXy+nWRX6nJ9XT9pPzPf/AF/m+DoOn5dD7vrtE/qLWlImO0+nr42ifi6Dhf2J+4vbOYmZrtHp/wCeJ9sdqTPboHeOXH2/XSs2tP8A5pvH+Wvuv2/h3noOza1RS8w/1OLkrOlPr+E1tb4qTbvToKeM/j7P+Sax/tyx/dH8+g//2Q==") repeat; 5 | margin-top: 40px; 6 | } 7 | 8 | header { 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | height: 40px; 13 | width: 96%; 14 | padding: 20px 2% 0; 15 | z-index: 999999; 16 | background: url("data:image/gif;base64,R0lGODlhBAAEAJEAABERESIiIgcHBwAAACH5BAAAAAAALAAAAAAEAAQAAAIHBCQRiIZaAAA7") repeat; 17 | } 18 | 19 | header h1 { 20 | margin: 0; 21 | color: white; 22 | font-size: 16px; 23 | width: 225px; 24 | float: left; 25 | } 26 | 27 | header h1:before { 28 | content: ''; 29 | height: 30px; 30 | width: 30px; 31 | margin: 5px 15px 5px 5px; 32 | background: url(../images/webdriver-robot.png); 33 | background-size: 30px; 34 | display: block; 35 | float:left; 36 | } 37 | 38 | header a { 39 | -webkit-border-radius: 5px; 40 | -moz-border-radius: 5px; 41 | -ms-border-radius: 5px; 42 | -o-border-radius: 5px; 43 | border-radius: 5px; 44 | background: #ea5906; 45 | color: white; 46 | display: block; 47 | float: right; 48 | text-decoration: none; 49 | padding: 3px 10px; 50 | text-align: center; 51 | border: 0; 52 | margin: 7px 3px; 53 | } 54 | 55 | .page { 56 | padding: 80px 15px 15px; 57 | } 58 | 59 | h2 { 60 | font-size: 18px; 61 | } 62 | 63 | .box { 64 | width: 100px; 65 | height: 100px; 66 | float: left; 67 | border: magenta 1px solid; 68 | margin-right: 10px; 69 | } 70 | #githubRepo { 71 | display:block; 72 | } 73 | .red { background: red; } 74 | .green { background: green; } 75 | .yellow { background: yellow; } 76 | .black { background: black; } 77 | .purple { background: purple; } 78 | 79 | .overlay { 80 | background: none repeat scroll 0 0 #CCCCCC; 81 | height: 50px; 82 | width: 100px; 83 | z-index: 2; 84 | opacity: 0.5; 85 | position: relative; 86 | } 87 | .btn3 { 88 | margin: 15px; 89 | position: relative; 90 | z-index: 1; 91 | bottom: 50px; 92 | } 93 | .btn1_clicked,.btn2_clicked,.btn3_clicked,.btn4_clicked, 94 | .btn1_dblclicked,.btn2_dblclicked,.btn3_dblclicked,.btn4_dblclicked, 95 | .btn1_middle_clicked, .btn1_right_clicked { 96 | display:none; 97 | } 98 | 99 | .container { position: relative; } 100 | 101 | .hitarea { 102 | position: absolute; 103 | left: 0; 104 | top: 0; 105 | width: 100%; 106 | height: 100%; 107 | background: rgba(0,0,0,.05); 108 | text-align: center; 109 | border: dashed 3px red; 110 | } 111 | 112 | 113 | .log li { 114 | display: inline-block; 115 | width: 49%; 116 | overflow: hidden; 117 | } 118 | 119 | @media screen and (min-width: 640px) { 120 | .log li { width: 30%; } 121 | } 122 | 123 | .properties li { white-space: nowrap; } 124 | .properties span { margin-left: 5px; } 125 | 126 | .log li.active { background: lightgreen; } 127 | 128 | .log li.property-gesture { 129 | position: fixed; 130 | right: 0; 131 | top: 0; 132 | background: lightgreen; 133 | padding: 1px 4px; 134 | width: auto; 135 | } 136 | .notLoaded { 137 | visibility: hidden; 138 | background-color: green; 139 | width: 80px; 140 | } 141 | .iamdownhere { 142 | margin-top: 3000px; 143 | margin-left: 3000px; 144 | background: black; 145 | width: 80px; 146 | } 147 | -------------------------------------------------------------------------------- /test/site/four.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | four 5 | 6 | 7 |
8 |
9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /test/site/gestureTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | two 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

WebdriverIO Testpage

14 | 2 15 | 1 16 |
17 | 18 |
19 |

20 | 23 |

24 |
25 |
26 |

Events

27 |
    28 |
  • touch
  • 29 |
  • release
  • 30 |
  • hold
  • 31 |
  • tap
  • 32 |
  • doubletap
  • 33 | 34 |
  • dragstart
  • 35 |
  • drag
  • 36 |
  • dragend
  • 37 |
  • dragleft
  • 38 |
  • dragright
  • 39 |
  • dragup
  • 40 |
  • dragdown
  • 41 | 42 |
  • swipe
  • 43 |
  • swipeleft
  • 44 |
  • swiperight
  • 45 |
  • swipeup
  • 46 |
  • swipedown
  • 47 | 48 |
  • transformstart
  • 49 |
  • transform
  • 50 |
  • transformend
  • 51 |
  • rotate
  • 52 |
  • rotateleft
  • 53 |
  • rotateright
  • 54 |
  • pinch
  • 55 |
  • pinchin
  • 56 |
  • pinchout
  • 57 |
58 |

EventData

59 |
    60 |
  • gesture
  • 61 |
  • touches
  • 62 |
  • pointerType
  • 63 |
  • center
  • 64 |
  • angle °
  • 65 |
  • direction
  • 66 |
  • distance px
  • 67 | 68 |
  • deltaTime ms
  • 69 |
  • deltaX px
  • 70 |
  • deltaY px
  • 71 | 72 |
  • velocityX
  • 73 |
  • velocityY
  • 74 | 75 |
  • scale
  • 76 |
  • rotation °
  • 77 | 78 |
  • target
  • 79 |
80 |
81 | 82 |
83 |
84 |
85 | 86 | 87 | 88 | 89 | 90 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /test/site/images/webdriver-robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/site/images/webdriver-robot.png -------------------------------------------------------------------------------- /test/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebdriverIO Testpage 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

WebdriverJS Testpage

14 | 2 15 | 1 16 |
17 | 18 |
19 |

Test CSS Attributes

20 | 21 | two 22 | GitHub Repo 23 | open new tab 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |

nested elements

35 |
36 | nested span 37 |
38 |
39 | 40 |
41 | 42 | 43 |
Button #1 clicked
44 |
Button #1 middle-clicked
45 |
Button #1 right-clicked
46 |
Button #1 dblclicked
47 | 48 |
Button #2 clicked
49 |
Button #2 dblclicked
50 |
 
51 | 52 |
Button #3 clicked
53 |
Button #3 dblclicked
54 | 55 |
Button #4 clicked
56 |
Button #4 dblclicked
57 | 58 |
59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | 92 | 93 | 94 |
95 | 96 | 97 | 98 | 99 | 100 | 169 |
I need some time to load
170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /test/site/three.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | two 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

WebdriverIO Testpage

14 | 2 15 | 1 16 |
17 | 18 |
19 |
20 | third page 21 |
22 |
23 | 24 | 25 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/site/two.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | two 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

WebdriverIO Testpage

14 | 2 15 | 1 16 |
17 | 18 |
19 | Second page 20 |
21 | 22 | 23 | 38 | 39 |
Hey, I am down here!
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/spec/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-boneyard/webdrivercss/71979a0c1684934c82b3da8ccc3bf48ca15922bf/test/spec/.DS_Store -------------------------------------------------------------------------------- /test/spec/exclude.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS should exclude parts of websites to ignore changing content', function() { 2 | 3 | before(function(done) { 4 | 5 | this.browser = WebdriverIO.remote(capabilities); 6 | 7 | // init plugin 8 | WebdriverCSS.init(this.browser); 9 | 10 | this.browser 11 | .init() 12 | .windowHandleSize({ width: 800, height: 600 }) 13 | .call(done); 14 | 15 | }); 16 | 17 | it('should exclude constantly changing content using CSS selectors', function(done) { 18 | this.browser 19 | .url(testurlThree) 20 | .webdrivercss('excludeUsingCssSelectors', { 21 | elem: '.third', 22 | exclude: '.third', 23 | name: '_' 24 | }) 25 | .call(function() { 26 | gm.compare('webdrivercss/excludeUsingCssSelectors._.baseline.png', 'test/fixtures/excludeElem.png', function (err, isEqual, equality, raw) { 27 | should.not.exist(err); 28 | isEqual.should.be.equal(true); 29 | equality.should.be.within(0, 0.0001); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should exclude constantly changing content using xPath selectors', function(done) { 36 | this.browser 37 | .url(testurlThree) 38 | .webdrivercss('excludeUsingXPath', { 39 | elem: '//html/body/section', 40 | exclude: '//html/body/section', 41 | name: '_' 42 | }) 43 | .call(function() { 44 | gm.compare('webdrivercss/excludeUsingXPath._.baseline.png', 'test/fixtures/excludeElem.png', function (err, isEqual, equality, raw) { 45 | should.not.exist(err); 46 | isEqual.should.be.equal(true); 47 | equality.should.be.within(0, 0.0001); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | it('should exclude constantly changing content using single xy rectangle', function(done) { 54 | this.browser 55 | .url(testurlThree) 56 | .webdrivercss('excludeUsingXYParameters', { 57 | elem: '.third', 58 | exclude: { 59 | x0: 0, 60 | x1: 230, 61 | y0: 60, 62 | y1: 295 63 | }, 64 | name: '_' 65 | }) 66 | .call(function() { 67 | gm.compare('webdrivercss/excludeUsingXYParameters._.baseline.png', 'test/fixtures/excludeElem.png', function (err, isEqual, equality, raw) { 68 | should.not.exist(err); 69 | isEqual.should.be.equal(true); 70 | equality.should.be.within(0, 0.0001); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | it('should exclude constantly changing content using multiple xy rectangles', function(done) { 77 | this.browser 78 | .url(testurlThree) 79 | .webdrivercss('excludeMultipleXYParameters', { 80 | elem: '.third', 81 | exclude: [{ 82 | x0: 0, 83 | x1: 115, 84 | y0: 60, 85 | y1: 295 86 | }, { 87 | x0: 115, 88 | x1: 230, 89 | y0: 60, 90 | y1: 295 91 | }], 92 | name: '_' 93 | }) 94 | .call(function() { 95 | gm.compare('webdrivercss/excludeMultipleXYParameters._.baseline.png', 'test/fixtures/excludeElem.png', function (err, isEqual, equality, raw) { 96 | should.not.exist(err); 97 | isEqual.should.be.equal(true); 98 | equality.should.be.within(0, 0.0001); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | it('should exclude constantly changing content using multiple xy points', function(done) { 105 | this.browser 106 | .url(testurlThree) 107 | .webdrivercss('excludeMultipleXYPoints', { 108 | elem: '.third', 109 | exclude: [{ 110 | x0: 0, 111 | y0: 60, 112 | x1: 100, 113 | y1: 60, 114 | x2: 100, 115 | y2: 260, 116 | x3: 0, 117 | y3: 260 118 | }, { 119 | x0: 100, 120 | y0: 60, 121 | x1: 200, 122 | y1: 60, 123 | x2: 200, 124 | y2: 260, 125 | x3: 100, 126 | y3: 260 127 | }], 128 | name: '_' 129 | }) 130 | .call(function() { 131 | gm.compare('webdrivercss/excludeMultipleXYPoints._.baseline.png', 'test/fixtures/excludeElem.png', function (err, isEqual, equality, raw) { 132 | should.not.exist(err); 133 | isEqual.should.be.equal(true); 134 | equality.should.be.within(0, 0.0001); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | after(afterHook); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /test/spec/hide.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS should hide parts of websites to ignore changing content', function() { 2 | 3 | before(function(done) { 4 | 5 | this.browser = WebdriverIO.remote(capabilities); 6 | 7 | // init plugin 8 | WebdriverCSS.init(this.browser); 9 | 10 | this.browser 11 | .init() 12 | .windowHandleSize({ width: 800, height: 600 }) 13 | .call(done); 14 | 15 | }); 16 | 17 | it('should hide constantly changing content using CSS selectors', function(done) { 18 | this.browser 19 | .url(testurlThree) 20 | .webdrivercss('hideUsingCssSelectors', { 21 | elem: '.third', 22 | hide: '.third', 23 | name: '_' 24 | }) 25 | .call(function() { 26 | gm.compare('webdrivercss/hideUsingCssSelectors._.baseline.png', 'test/fixtures/hideElem.png', function (err, isEqual, equality, raw) { 27 | should.not.exist(err); 28 | equality.should.be.within(0, 0.0001); 29 | isEqual.should.be.equal(true); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should exclude constantly changing content using xPath selectors', function(done) { 36 | this.browser 37 | .url(testurlThree) 38 | .webdrivercss('hideUsingXPath', { 39 | elem: '//html/body/section', 40 | hide: '//html/body/section', 41 | name: '_' 42 | }) 43 | .call(function() { 44 | gm.compare('webdrivercss/hideUsingXPath._.baseline.png', 'test/fixtures/hideElem.png', function (err, isEqual, equality, raw) { 45 | should.not.exist(err); 46 | equality.should.be.within(0, 0.0001); 47 | isEqual.should.be.equal(true); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | after(afterHook); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/spec/imageCapturing.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS captures desired parts of a website as screenshot with specific dimension', function() { 2 | 3 | before(function(done) { 4 | 5 | this.browser = WebdriverIO.remote(capabilities); 6 | 7 | // init plugin 8 | WebdriverCSS.init(this.browser); 9 | 10 | this.browser 11 | .init() 12 | .url(testurl) 13 | .windowHandleSize({ width: 800, height: 600 }) 14 | .call(done); 15 | 16 | }); 17 | 18 | describe('should do a screenshot of a whole website if nothing specified', function(done) { 19 | 20 | var documentHeight = 0; 21 | 22 | before(function(done) { 23 | this.browser 24 | .webdrivercss('testWithoutParameter', { name: 'withoutParams'}) 25 | .execute(function(){ 26 | return document.body.clientHeight; 27 | }, function(err,res) { 28 | documentHeight = res.value; 29 | }) 30 | .call(done); 31 | }); 32 | 33 | it('should exist an image in the default image folder', function(done) { 34 | fs.exists('webdrivercss/testWithoutParameter.withoutParams.baseline.png', function(exists) { 35 | exists.should.equal(true); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should have the size of browser dimension', function(done) { 41 | gm('webdrivercss/testWithoutParameter.withoutParams.baseline.png').size(function(err,size) { 42 | should.not.exist(err); 43 | size.width.should.be.equal(800); 44 | // It's not clear why image height is slightly different from document height in 45 | // some environments. See issue #76. 46 | size.height.should.be.within(documentHeight - 20, documentHeight); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should equal to fixture image', function(done) { 52 | gm.compare('webdrivercss/testWithoutParameter.withoutParams.baseline.png', 'test/fixtures/testWithoutParameter.current.png', function (err, isEqual, equality, raw) { 53 | should.not.exist(err); 54 | isEqual.should.be.equal(true); 55 | done(); 56 | }); 57 | }); 58 | 59 | }); 60 | 61 | describe('should do a screenshot with specific width and height values', function(done) { 62 | 63 | before(function(done) { 64 | this.browser 65 | .webdrivercss('testWithWidthHeightParameter', { 66 | name: '_', 67 | x: 100, 68 | y: 100, 69 | width: 100, 70 | height: 100 71 | }) 72 | .call(done); 73 | }); 74 | 75 | it('should exist an image in the default image folder', function(done) { 76 | fs.exists('webdrivercss/testWithWidthHeightParameter._.baseline.png', function(exists) { 77 | exists.should.equal(true); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should have same size like given parameter', function(done) { 83 | gm('webdrivercss/testWithWidthHeightParameter._.baseline.png').size(function(err,size) { 84 | should.not.exist(err); 85 | size.width.should.be.equal(100); 86 | size.height.should.be.equal(100); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should equal to fixture image', function(done) { 92 | gm.compare('webdrivercss/testWithWidthHeightParameter._.baseline.png', 'test/fixtures/testWithWidthHeightParameter.current.png', function (err, isEqual, equality, raw) { 93 | should.not.exist(err); 94 | isEqual.should.be.equal(true); 95 | done(); 96 | }); 97 | }); 98 | 99 | }); 100 | 101 | describe('should do a screenshot of a given element', function(done) { 102 | 103 | before(function(done) { 104 | this.browser 105 | .webdrivercss('testWithGivenElement', { 106 | elem: '.red', 107 | name: '_' 108 | }) 109 | .call(done); 110 | }); 111 | 112 | it('should exist an image in the default image folder', function(done) { 113 | fs.exists('webdrivercss/testWithGivenElement._.baseline.png', function(exists) { 114 | exists.should.equal(true); 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should have the size of given element', function(done) { 120 | gm('webdrivercss/testWithGivenElement._.baseline.png').size(function(err,size) { 121 | should.not.exist(err); 122 | size.width.should.be.equal(102); 123 | size.height.should.be.equal(102); 124 | done(); 125 | }); 126 | }); 127 | 128 | it('should equal to fixture image', function(done) { 129 | gm.compare('webdrivercss/testWithGivenElement._.baseline.png', 'test/fixtures/testWithGivenElement.current.png', function (err, isEqual, equality, raw) { 130 | should.not.exist(err); 131 | isEqual.should.be.equal(true); 132 | done(); 133 | }); 134 | }); 135 | 136 | }); 137 | 138 | describe('should do a screenshot of multiple elements', function(done) { 139 | var optsArrayOrig = [ 140 | { 141 | elem: '.red', 142 | name: 'red' 143 | }, { 144 | elem: '.green', 145 | name: 'green' 146 | }]; 147 | var optsArrayClone = JSON.parse(JSON.stringify(optsArrayOrig)); 148 | 149 | before(function(done) { 150 | this.browser 151 | .webdrivercss('testWithMultipleElement', optsArrayClone) 152 | .call(done); 153 | }); 154 | 155 | it('should exist two images in the default image folder', function(done) { 156 | fs.existsSync('webdrivercss/testWithMultipleElement.png').should.equal(true); 157 | fs.existsSync('webdrivercss/testWithMultipleElement.red.baseline.png').should.equal(true); 158 | fs.existsSync('webdrivercss/testWithMultipleElement.green.baseline.png').should.equal(true); 159 | done(); 160 | }); 161 | 162 | it('should not change the array passed in', function(done) { 163 | optsArrayClone.should.deep.equal(optsArrayOrig); 164 | done(); 165 | }); 166 | 167 | }); 168 | 169 | describe('should do a screenshot of a given element with given width/height', function(done) { 170 | 171 | var documentHeight = 0; 172 | 173 | before(function(done) { 174 | this.browser 175 | .webdrivercss('testWithGivenElementAndWidthHeight', { 176 | elem: '.yellow', 177 | name: '_', 178 | width: 550, 179 | height: 102 180 | }) 181 | .call(done); 182 | }); 183 | 184 | it('should exist an image in the default image folder', function(done) { 185 | fs.exists('webdrivercss/testWithGivenElementAndWidthHeight._.baseline.png', function(exists) { 186 | exists.should.equal(true); 187 | done(); 188 | }); 189 | }); 190 | 191 | it('should have the size of given element', function(done) { 192 | gm('webdrivercss/testWithGivenElementAndWidthHeight._.baseline.png').size(function(err,size) { 193 | should.not.exist(err); 194 | size.width.should.be.equal(550); 195 | size.height.should.be.equal(102); 196 | done(); 197 | }); 198 | }); 199 | 200 | it('should equal to fixture image', function(done) { 201 | gm.compare('webdrivercss/testWithGivenElementAndWidthHeight._.baseline.png', 'test/fixtures/testWithGivenElementAndWidthHeight.current.png', function (err, isEqual, equality, raw) { 202 | should.not.exist(err); 203 | isEqual.should.be.equal(true); 204 | done(); 205 | }); 206 | }); 207 | 208 | }); 209 | 210 | describe('should do a screenshot at specific x,y position with specific width,height', function(done) { 211 | 212 | var documentHeight = 0; 213 | 214 | before(function(done) { 215 | this.browser 216 | .webdrivercss('testAtSpecificPosition', { 217 | x: 15, 218 | y: 15, 219 | width: 230, 220 | height: 50, 221 | name: '_' 222 | }) 223 | .call(done); 224 | }); 225 | 226 | it('should exist an image in the default image folder', function(done) { 227 | fs.exists('webdrivercss/testAtSpecificPosition._.baseline.png', function(exists) { 228 | exists.should.equal(true); 229 | done(); 230 | }); 231 | }); 232 | 233 | it('should have same size like given parameter', function(done) { 234 | gm('webdrivercss/testAtSpecificPosition._.baseline.png').size(function(err,size) { 235 | should.not.exist(err); 236 | size.width.should.be.equal(230); 237 | size.height.should.be.equal(50); 238 | done(); 239 | }); 240 | }); 241 | 242 | it('should equal to fixture image', function(done) { 243 | gm.compare('webdrivercss/testAtSpecificPosition._.baseline.png', 'test/fixtures/testAtSpecificPosition.current.png', function (err, isEqual, equality, raw) { 244 | should.not.exist(err); 245 | isEqual.should.be.equal(true); 246 | done(); 247 | }); 248 | }); 249 | 250 | }); 251 | 252 | describe('should capture areas which are not within viewport', function() { 253 | 254 | it('using elem option', function(done) { 255 | 256 | this.browser 257 | .url(testurlTwo) 258 | .webdrivercss('notWithinViewportElem', { 259 | elem: '.iamdownhere', 260 | name: '_' 261 | }) 262 | .call(function() { 263 | gm.compare('webdrivercss/notWithinViewportElem._.baseline.png', 'test/fixtures/notWithinViewport.png', function (err, isEqual, equality, raw) { 264 | should.not.exist(err); 265 | isEqual.should.be.equal(true); 266 | done(); 267 | }); 268 | }); 269 | 270 | }); 271 | 272 | it('using xy coordinates', function(done) { 273 | 274 | this.browser 275 | .url(testurlTwo) 276 | .webdrivercss('notWithinViewportXY', { 277 | x: 3000, 278 | y: 3295, 279 | width: 80, 280 | height: 40, 281 | name: '_' 282 | }) 283 | .call(function() { 284 | gm.compare('webdrivercss/notWithinViewportXY._.baseline.png', 'test/fixtures/notWithinViewport.png', function (err, isEqual, equality, raw) { 285 | should.not.exist(err); 286 | isEqual.should.be.equal(true); 287 | done(); 288 | }); 289 | }); 290 | 291 | }); 292 | 293 | }); 294 | 295 | after(afterHook); 296 | 297 | }); 298 | -------------------------------------------------------------------------------- /test/spec/imageComparison.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS compares images and exposes information about CSS regression', function() { 2 | 3 | var capturingData = { 4 | elem: '.yellow', 5 | name: 'test-component', 6 | width: 550, 7 | height: 102 8 | }; 9 | 10 | before(function(done) { 11 | 12 | this.browser = WebdriverIO.remote(capabilities); 13 | 14 | // init plugin 15 | WebdriverCSS.init(this.browser); 16 | 17 | this.browser 18 | .init() 19 | .url(testurl) 20 | .windowHandleSize({ width: 800, height: 600 }) 21 | .call(done); 22 | 23 | }); 24 | 25 | describe('should take a screenshot of same area without any changes in it', function(done) { 26 | var resultObject; 27 | 28 | before(function(done) { 29 | this.browser 30 | .webdrivercss('comparisonTest', capturingData, function(err, res) { 31 | should.not.exist(err); 32 | resultObject = res[capturingData.name][0]; 33 | }) 34 | .call(done); 35 | }); 36 | 37 | it('should exist an image (*.baseline.png) in the default image folder', function(done) { 38 | fs.exists(resultObject.baselinePath, function(exists) { 39 | exists.should.equal(true); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should NOT exist an image (*.regression.png) in the default image folder', function(done) { 45 | fs.exists('webdrivercss/comparisonTest.test-component.regression.png', function(exists) { 46 | exists.should.equal(false); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should NOT exist an image (*.diff.png) in the default failed comparisons image folder', function(done) { 52 | fs.exists('webdrivercss/diff/comparisonTest.test-component.diff.png', function(exists) { 53 | exists.should.equal(false); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should return a proper result object', function() { 59 | resultObject.misMatchPercentage.should.equal(0); 60 | resultObject.isExactSameImage.should.equal(true); 61 | resultObject.isSameDimensions.should.equal(true); 62 | resultObject.isWithinMisMatchTolerance.should.equal(true); 63 | }); 64 | 65 | }); 66 | 67 | describe('should change something within given area to do an image diff', function() { 68 | var resultObject = {}; 69 | 70 | before(function(done) { 71 | this.browser 72 | .execute(function() { 73 | document.querySelector('.green').style.backgroundColor = 'white'; 74 | document.querySelector('.black').style.backgroundColor = 'white'; 75 | },[]) 76 | .webdrivercss('comparisonTest', capturingData, function(err,res) { 77 | should.not.exist(err); 78 | resultObject = res[capturingData.name][0]; 79 | }) 80 | .call(done); 81 | }); 82 | 83 | it('should exist an image (*.baseline.png) in the default image folder', function(done) { 84 | fs.exists(resultObject.baselinePath, function(exists) { 85 | exists.should.equal(true); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should exist an image (*.regression.png) in the default image folder', function(done) { 91 | fs.exists(resultObject.regressionPath, function(exists) { 92 | exists.should.equal(true); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should exist an image (*.diff.png) in the default failed comparisons image folder', function(done) { 98 | fs.exists(resultObject.diffPath, function(exists) { 99 | exists.should.equal(true); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should exist an *.diff image with same dimension', function() { 105 | resultObject.isSameDimensions.should.be.a('boolean'); 106 | resultObject.isSameDimensions.should.equal(true); 107 | }); 108 | 109 | it('should have an mismatch percentage of 35.65%', function() { 110 | resultObject.misMatchPercentage.should.be.a('number'); 111 | resultObject.misMatchPercentage.should.equal(35.65); 112 | resultObject.isExactSameImage.should.equal(false); 113 | resultObject.isWithinMisMatchTolerance.should.equal(false); 114 | }); 115 | 116 | }); 117 | 118 | describe('should take a screenshot of same area without any changes in it', function() { 119 | var resultObject = {}; 120 | 121 | before(function(done) { 122 | this.browser 123 | .webdrivercss('comparisonTest', capturingData, function(err,res) { 124 | should.not.exist(err); 125 | resultObject = res[capturingData.name][0]; 126 | }) 127 | .call(done); 128 | }); 129 | 130 | it('should exist an image (*.baseline.png) in the default image folder', function(done) { 131 | fs.exists(resultObject.baselinePath, function(exists) { 132 | exists.should.equal(true); 133 | done(); 134 | }); 135 | }); 136 | 137 | it('should exist an image (*.regression.png) in the default image folder', function(done) { 138 | fs.exists(resultObject.regressionPath, function(exists) { 139 | exists.should.equal(true); 140 | done(); 141 | }); 142 | }); 143 | 144 | it('should exist an image (*.diff.png) in the default failed comparisons image folder', function(done) { 145 | fs.exists(resultObject.diffPath, function(exists) { 146 | exists.should.equal(true); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('should exist an *.diff image with same dimension', function() { 152 | resultObject.isSameDimensions.should.be.a('boolean'); 153 | resultObject.isSameDimensions.should.equal(true); 154 | }); 155 | 156 | it('should have an mismatch percentage of 35.65%', function() { 157 | resultObject.misMatchPercentage.should.be.a('number'); 158 | resultObject.misMatchPercentage.should.equal(35.65); 159 | resultObject.isExactSameImage.should.equal(false); 160 | resultObject.isWithinMisMatchTolerance.should.equal(false); 161 | }); 162 | 163 | }); 164 | 165 | describe('updates baseline if updateBaseImages is given', function(done) { 166 | var resultObject = {}; 167 | 168 | before(function(done) { 169 | capturingData.updateBaseline = true; 170 | 171 | this.browser 172 | .webdrivercss('comparisonTest', capturingData, function(err,res) { 173 | should.not.exist(err); 174 | resultObject = res[capturingData.name][0]; 175 | }) 176 | .call(done); 177 | }); 178 | 179 | it('should exist an image (*.baseline.png) in the default image folder', function(done) { 180 | fs.exists(resultObject.baselinePath, function(exists) { 181 | exists.should.equal(true); 182 | done(); 183 | }); 184 | }); 185 | 186 | it('should NOT exist an image (*.regression.png) in the default image folder', function(done) { 187 | fs.exists('webdrivercss/comparisonTest.test-component.regression.png', function(exists) { 188 | exists.should.equal(false); 189 | done(); 190 | }); 191 | }); 192 | 193 | it('should NOT exist an image (*.diff.png) in the default failed comparisons image folder', function(done) { 194 | fs.exists('webdrivercss/diff/comparisonTest.test-component.diff.png', function(exists) { 195 | exists.should.equal(false); 196 | done(); 197 | }); 198 | }); 199 | 200 | it('should return a proper result object', function() { 201 | resultObject.misMatchPercentage.should.equal(0); 202 | resultObject.isExactSameImage.should.equal(true); 203 | resultObject.isSameDimensions.should.equal(true); 204 | resultObject.isWithinMisMatchTolerance.should.equal(true); 205 | }); 206 | 207 | }); 208 | 209 | describe('should match an image when match percentage is equal to tolerance', function() { 210 | var resultObject = {}; 211 | 212 | before(function(done) { 213 | this.browser 214 | .url(testurlFour) 215 | .webdrivercss('comparisonTest', { 216 | elem: '#container', 217 | name: 'test-equal' 218 | }, function(err,res) { 219 | should.not.exist(err); 220 | }) 221 | .execute(function() { 222 | document.querySelector('#difference').style.backgroundColor = 'white'; 223 | }, []) 224 | .webdrivercss('comparisonTest', { 225 | elem: '#container', 226 | name: 'test-equal' 227 | }, function(err,res) { 228 | should.not.exist(err); 229 | resultObject = res['test-equal'][0]; 230 | }) 231 | .call(done); 232 | }); 233 | 234 | it('should be within tolerance', function() { 235 | resultObject.misMatchPercentage.should.be.a('number'); 236 | resultObject.misMatchPercentage.should.equal(0.05); 237 | resultObject.isExactSameImage.should.equal(false); 238 | resultObject.isWithinMisMatchTolerance.should.equal(true); 239 | }); 240 | 241 | }); 242 | 243 | after(afterHook); 244 | 245 | }); 246 | -------------------------------------------------------------------------------- /test/spec/instantiation.js: -------------------------------------------------------------------------------- 1 | /*jshint -W030 */ 2 | 3 | describe('WebdriverCSS plugin as WebdriverIO enhancement', function() { 4 | 5 | before(function(done) { 6 | this.browser = WebdriverIO.remote(capabilities).init(done); 7 | }); 8 | 9 | it('should not exist as command in WebdriverIO instance without initialization', function() { 10 | should.not.exist(this.browser.webdrivercss); 11 | }); 12 | 13 | it('should not have any created folder before initialization', function(done) { 14 | fs.exists('webdrivercss', function(exists) { 15 | exists.should.be.equal(false); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should throw an error on initialization without passing WebdriverIO instance', function() { 21 | expect(WebdriverCSS.init).to.throw(Error, 'A WebdriverIO instance is needed to initialise WebdriverCSS'); 22 | }); 23 | 24 | it('should be initialized without errors', function() { 25 | WebdriverCSS.init(this.browser).should.not.throw; 26 | }); 27 | 28 | it('should enhance WebdriverIO instance with "webdrivercss" command after initialization', function() { 29 | should.exist(this.browser.webdrivercss); 30 | }); 31 | 32 | it('should contain some default values', function() { 33 | var plugin = WebdriverCSS.init(this.browser); 34 | 35 | expect(plugin).to.have.property('screenshotRoot').to.equal('webdrivercss'); 36 | expect(plugin).to.have.property('failedComparisonsRoot').to.equal('webdrivercss/diff'); 37 | expect(plugin).to.have.property('misMatchTolerance').to.equal(0.05); 38 | 39 | }); 40 | 41 | it('should contain some custom values', function() { 42 | var plugin = WebdriverCSS.init(this.browser, { 43 | screenshotRoot: '__screenshotRoot__', 44 | failedComparisonsRoot: '__failedComparisonsRoot__', 45 | misMatchTolerance: 50 46 | }); 47 | 48 | expect(plugin).to.have.property('screenshotRoot').to.equal('__screenshotRoot__'); 49 | expect(plugin).to.have.property('failedComparisonsRoot').to.equal('__failedComparisonsRoot__'); 50 | expect(plugin).to.have.property('misMatchTolerance').to.equal(50); 51 | }); 52 | 53 | it('should have a created "screenshotRoot" folder after initialization', function(done) { 54 | fs.exists('__screenshotRoot__', function(exist) { 55 | exist.should.be.equal(true); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should have a created "failedComparisonsRoot" folder after initialization', function(done) { 61 | fs.exists('__failedComparisonsRoot__', function(exist) { 62 | exist.should.be.equal(true); 63 | done(); 64 | }); 65 | }); 66 | 67 | after(afterHook); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/spec/remove.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS should remove parts of websites to ignore changing content', function() { 2 | 3 | before(function(done) { 4 | 5 | this.browser = WebdriverIO.remote(capabilities); 6 | 7 | // init plugin 8 | WebdriverCSS.init(this.browser); 9 | 10 | this.browser 11 | .init() 12 | .windowHandleSize({ width: 800, height: 600 }) 13 | .call(done); 14 | 15 | }); 16 | 17 | it('should remove constantly changing content using CSS selectors', function(done) { 18 | this.browser 19 | .url(testurlThree) 20 | .webdrivercss('removeUsingCssSelectors', { 21 | elem: '#third', 22 | remove: '.third', 23 | name: '_' 24 | }) 25 | .call(function() { 26 | gm.compare('webdrivercss/removeUsingCssSelectors._.baseline.png', 'test/fixtures/hideElem.png', function (err, isEqual, equality, raw) { 27 | should.not.exist(err); 28 | equality.should.be.within(0, 0.0001); 29 | isEqual.should.be.equal(true); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should exclude constantly changing content using xPath selectors', function(done) { 36 | this.browser 37 | .url(testurlThree) 38 | .webdrivercss('removeUsingXPath', { 39 | elem: '//html/body/section', 40 | remove: '//html/body/section/div', 41 | name: '_' 42 | }) 43 | .call(function() { 44 | gm.compare('webdrivercss/removeUsingXPath._.baseline.png', 'test/fixtures/hideElem.png', function (err, isEqual, equality, raw) { 45 | should.not.exist(err); 46 | equality.should.be.within(0, 0.0001); 47 | isEqual.should.be.equal(true); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | after(afterHook); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/spec/screenWidth.js: -------------------------------------------------------------------------------- 1 | describe('WebdriverCSS captures shots with different screen widths', function() { 2 | var resultObject; 3 | 4 | before(function(done) { 5 | this.browser = WebdriverIO.remote(capabilities); 6 | 7 | // init plugin 8 | WebdriverCSS.init(this.browser); 9 | 10 | this.browser 11 | .init() 12 | .url(testurl) 13 | .windowHandleSize({ width: 999, height: 999 }) 14 | .webdrivercss('screenWidthTest', [ 15 | { 16 | name: 'test', 17 | screenWidth: [320,480,640,1024] 18 | },{ 19 | name: 'test_two', 20 | screenWidth: [444,666] 21 | } 22 | ], function(err, res) { 23 | should.not.exist(err); 24 | resultObject = res; 25 | }) 26 | .call(done); 27 | 28 | }); 29 | 30 | /** 31 | * 12 pictures get taken 32 | * - 4 + 2 cropped images of the element for each screen resolution 33 | * - 6 screenshots of the whole website for each screen resolution 34 | */ 35 | it('if 4 screen widths are given and 2 elements captured, it should have taken 12 shots', function(done) { 36 | glob('webdrivercss/*.png', function(err,files) { 37 | should.not.exist(err); 38 | files.should.have.length(12); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('screen width should be part of file name', function(done) { 44 | glob('webdrivercss/*.png', function(err,files) { 45 | should.not.exist(err); 46 | files.forEach(function(file,i) { 47 | file.match(/(.)+\.\d+px(\.baseline)*\.png/g).should.have.length(1); 48 | }); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('shots should have a specific width according to given screen width', function(done) { 54 | glob('webdrivercss/*.png', function(err,files) { 55 | should.not.exist(err); 56 | files.forEach(function(file,i) { 57 | var width = parseInt(file.match(/\d+/g)[0],10); 58 | gm('webdrivercss/screenWidthTest.' + width + 'px.png').size(function(err,size) { 59 | should.not.exist(err); 60 | 61 | // travisci made me do that -.- 62 | if(size.width === 321) size.width = 320; 63 | 64 | size.width.should.be.equal(width); 65 | 66 | if(i === files.length - 1) done(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | it('browser should be get back to old resolution after shots were taken', function(done) { 73 | this.browser 74 | .windowHandleSize(function(err,res) { 75 | should.not.exist(err); 76 | res.value.width.should.be.equal(999); 77 | res.value.height.should.be.equal(999); 78 | }) 79 | .call(done); 80 | }); 81 | 82 | describe('returns a result object with proper test results', function() { 83 | 84 | it('should contain results of both elements', function() { 85 | expect(resultObject.test).to.exist; 86 | expect(resultObject.test_two).to.exist; 87 | }); 88 | 89 | it('should contain result for each screenresolution', function() { 90 | expect(resultObject.test).to.have.length(4); 91 | expect(resultObject.test_two).to.have.length(2); 92 | }) 93 | 94 | }); 95 | 96 | after(afterHook); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /test/spec/sync.js: -------------------------------------------------------------------------------- 1 | /*jshint -W030 */ 2 | 3 | var fs = require('fs'), 4 | nock = require('nock'), 5 | path = require('path'); 6 | 7 | nock.enableNetConnect(); 8 | 9 | describe('WebdriverCSS should be able to', function() { 10 | 11 | describe('sync the image repository with an external API', function() { 12 | 13 | before(function(done) { 14 | this.browser = WebdriverIO.remote(capabilities); 15 | 16 | // init plugin 17 | var plugin = WebdriverCSS.init(this.browser, { 18 | api: 'http://127.0.0.1:8081/webdrivercss/api', 19 | user: 'johndoe', 20 | key: 'xyz' 21 | }); 22 | 23 | this.browser 24 | .init() 25 | .url(testurl) 26 | .call(done); 27 | }); 28 | 29 | it('throws an error if API isn\'t provided', function(done) { 30 | var browser = WebdriverIO.remote(capabilities); 31 | WebdriverCSS.init(browser); 32 | 33 | browser.init().sync(function(err) { 34 | expect(err).not.to.be.null; 35 | }).end(done); 36 | }); 37 | 38 | it('should download and unzip a repository by calling sync() for the first time', function(done) { 39 | 40 | var scope = nock('http://127.0.0.1:8081') 41 | .defaultReplyHeaders({ 42 | 'Content-Type': 'application/octet-stream' 43 | }) 44 | .get('/webdrivercss/api/webdrivercss.tar.gz') 45 | .reply(200, function(uri, requestBody) { 46 | return fs.createReadStream(path.join(__dirname, '..', 'fixtures', 'webdrivercss.tar.gz')); 47 | }); 48 | 49 | this.browser.sync().call(function() { 50 | expect(fs.existsSync(path.join(__dirname, '..', '..', 'webdrivercss'))).to.be.true; 51 | expect(fs.existsSync(path.join(__dirname, '..', '..', 'webdrivercss', 'comparisonTest.current.png'))).to.be.true; 52 | }).call(done); 53 | 54 | }); 55 | 56 | it('should zip and upload repository to API after test run', function(done) { 57 | 58 | var madeRequest = false; 59 | var scope = nock('http://127.0.0.1:8081') 60 | .defaultReplyHeaders({ 61 | 'Content-Type': 'application/octet-stream' 62 | }) 63 | .post('/webdrivercss/api') 64 | .reply(200, function(uri, requestBody) { 65 | madeRequest = true; 66 | }); 67 | 68 | this.browser 69 | .webdrivercss('testWithoutParameter', { 70 | name: 'test' 71 | }) 72 | .sync() 73 | .call(function() { 74 | expect(madeRequest).to.be.true; 75 | 76 | // should delete the tarball file after syncing 77 | expect(fs.existsSync(path.join(__dirname, '..', '..', 'webdrivercss.tar.gz'))).to.be.false; 78 | }) 79 | .call(done); 80 | 81 | }); 82 | 83 | after(afterHook); 84 | }); 85 | 86 | 87 | describe('sync images with applitools eyes', function() { 88 | var key = 'ABCDEFG12345', 89 | fakeSessionId = 123456789012345, 90 | applitoolsHost = 'https://eyessdk.applitools.com', 91 | isSessionInitiated = false, 92 | hasSyncedImage = false, 93 | updateBaseline = false, 94 | isSessionClosed = false; 95 | 96 | var headers = { 97 | 'Accept': 'application/json', 98 | 'Content-Type': 'application/json' 99 | } 100 | 101 | /** 102 | * mock session initiatlization 103 | */ 104 | nock(applitoolsHost, {reqheaders: headers}) 105 | .post('/api/sessions/running?apiKey=' + key) 106 | .reply(200, function(uri, requestBody) { 107 | isSessionInitiated = true; 108 | return { 109 | 'id': fakeSessionId, 110 | 'url': 'https://eyes.applitools.com/app/sessions/' + fakeSessionId 111 | }; 112 | }); 113 | 114 | /** 115 | * mock image sync 116 | */ 117 | nock(applitoolsHost, {reqheaders: headers}) 118 | .post('/api/sessions/running/' + fakeSessionId + '?apiKey=' + key) 119 | .reply(200, function(uri, requestBody) { 120 | hasSyncedImage = true; 121 | return { 'asExpected' : true }; 122 | }); 123 | 124 | /** 125 | * mock session end 126 | */ 127 | nock(applitoolsHost, {reqheaders: headers}) 128 | .delete('/api/sessions/running/' + fakeSessionId + '?apiKey=' + key + '&updateBaseline=' + updateBaseline) 129 | .reply(200, function(uri, requestBody) { 130 | isSessionClosed = true; 131 | return {'steps':1,'matches':1,'mismatches':0,'missing':0}; 132 | }); 133 | 134 | before(function(done) { 135 | this.browser = WebdriverIO.remote(capabilities); 136 | 137 | // init plugin 138 | var plugin = WebdriverCSS.init(this.browser, { 139 | key: key, 140 | updateBaseline: updateBaseline 141 | }); 142 | 143 | this.browser 144 | .init() 145 | .url(testurl) 146 | .webdrivercss('applitoolstest', { 147 | name: 'page' 148 | }, done); 149 | }); 150 | 151 | it('should throw an error if no app id is provided', function() { 152 | expect(isSessionInitiated).to.be.true; 153 | }); 154 | 155 | it('should sync images with applitools eyes', function() { 156 | expect(hasSyncedImage).to.be.true; 157 | }); 158 | 159 | it('should end applitools session', function() { 160 | expect(isSessionClosed).to.be.true; 161 | }); 162 | 163 | after(afterHook); 164 | }); 165 | }); 166 | --------------------------------------------------------------------------------