├── .gitattributes ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── casperjs.bat ├── demo ├── coffeemachine.html └── testsuite.js ├── package-lock.json ├── package.json ├── phantomcss.js ├── readme_assets ├── Phantom CSS.png ├── differentcolour.png ├── false-negative.png └── intro-example.png └── resemblejscontainer.html /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | screenshots 3 | failures 4 | .idea/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | libs 2 | demo 3 | readme_assets 4 | casperjs.bat 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright © 2013-2017 Huddle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **Unmaintained notice**: As of December 22nd 2017 this project will no longer be maintained. It's been a fantastic five years, a project that has hopefully had a positive influence on the shape and extent of Web UI testing. Read more on [why its time to move on](https://github.com/Huddle/PhantomCSS#why-is-this-project-no-longer-maintained). 3 | 4 | 5 | --- 6 | 7 | 8 | Cute image of a ghost 9 | 10 | **CSS regression testing**. A [CasperJS](http://github.com/n1k0/casperjs) module for automating visual regression testing with [PhantomJS 2](http://github.com/ariya/phantomjs/) or [SlimerJS](http://slimerjs.org/) and [Resemble.js](http://huddle.github.com/Resemble.js/). For testing Web apps, live style guides and responsive layouts. Read more on Huddle's Engineering blog: [CSS Regression Testing](http://tldr.huddle.com/blog/css-testing/). 11 | 12 | **Huddle is Hiring!** [We're looking for talented front-end engineers](https://talentcommunity.huddle.com/careers) 13 | 14 | ### What? 15 | 16 | PhantomCSS takes screenshots captured by CasperJS and compares them to baseline images using [Resemble.js](http://huddle.github.com/Resemble.js/) to test for rgb pixel differences. PhantomCSS then generates image diffs to help you find the cause. 17 | 18 | ![A failed visual regression test, pink areas show where padding has changed.](https://raw.github.com/Huddle/PhantomCSS/master/readme_assets/intro-example.png "Failed visual regression test") 19 | 20 | Screenshot based regression testing can only work when UI is predictable. It's possible to hide mutable UI components with PhantomCSS but it would be better to test static pages or drive the UI with faked data during test runs. 21 | 22 | ### Example 23 | 24 | ```javascript 25 | casper. 26 | start( url ). 27 | then(function(){ 28 | 29 | // do something 30 | casper.click('button#open-dialog'); 31 | 32 | // Take a screenshot of the UI component 33 | phantomcss.screenshot('#the-dialog', 'a screenshot of my dialog'); 34 | 35 | }); 36 | ``` 37 | 38 | From the command line/terminal run: 39 | 40 | * `casperjs test demo/testsuite.js` 41 | 42 | or 43 | 44 | * `casperjs test demo/testsuite.js --verbose --log-level=debug --xunit=results.xml` 45 | 46 | ### Updating from PhantomCSS v0 to v1 47 | 48 | Rendering is quite different with PhantomJS 2, so when you update, old visual tests will start failing. 49 | If your tests are green and passing before updating, I would recommend rebasing the visual tests, i.e. delete them, and run the test suite to create a new baseline. 50 | 51 | You can still use the [v0 branch](https://github.com/Huddle/PhantomCSS/tree/v0) if you wish, though it now unmaintained. 52 | 53 | ### Download 54 | 55 | PhantomCSS can be downloaded in various ways: 56 | 57 | * `npm install phantomcss` (PhantomCSS is not itself a Node.js module) 58 | * `bower install phantomcss` 59 | * `git clone git://github.com/Huddle/PhantomCSS.git` 60 | 61 | If you are not installing via NPM, you will need to run `npm install` in the PhantomCSS root folder. 62 | 63 | Please note that depending on how you have installed PhantomCSS you may need to set the libraryRoot configuration property to link to the directory in which phantomcss.js resides. 64 | 65 | ### Getting started, try the demo 66 | 67 | * For convenience I've included CasperJS.bat for Windows users. If you are not a Windows user, you will have to install the latest version of [CasperJS](http://docs.casperjs.org/en/latest/installation.html). 68 | * Download or clone this repo and run `casperjs test demo/testsuite.js` in command/terminal from the PhantomCSS folder. PhantomJS is the only binary dependency - this should just work 69 | * Find the screenshot folder and have a look at the (baseline) images 70 | * Run the tests again with `casperjs test demo/testsuite.js`. New screenshots will be created to compare against the baseline images. These new images can be ignored, they will be replaced every test run. 71 | * To test failure, add/change some CSS in the file demo/coffeemachine.html e.g. make `.mug` bright green 72 | * Run the tests again, you should see some reported failures 73 | * In the failures folder some images should have been created. The images should show bright pink where the screenshot has visually changed 74 | * If you want to manually compare the images, go to the screenshot folder to see the original/baseline and latest screenshots 75 | 76 | ### SlimerJS 77 | 78 | SlimerJS uses the Gecko browser engine rather than Webkit. This has some advantages over PhantomJS, such as a non-headless view. If this is of interest to you, please follow the [download and install](http://slimerjs.org/download.html) instructions and ensure SlimerJS is installed globally. 79 | 80 | * `casperjs test demo/testsuite.js --engine=slimerjs` 81 | 82 | ### Options and setup 83 | 84 | If you are using SlimerJS, you will need to specify absolute paths (see 'demo'). 85 | 86 | ```javascript 87 | phantomcss.init({ 88 | 89 | /* 90 | captureWaitEnabled defaults to true, setting to false will remove a small wait/delay on each 91 | screenshot capture - useful when you don't need to worry about 92 | animations and latency in your visual tests 93 | */ 94 | captureWaitEnabled: true, 95 | 96 | /* 97 | libraryRoot is now optional unless you are using SlimerJS where 98 | you will need to set it to the correct path. It must point to 99 | your phantomcss folder. If you are using NPM, this will probably 100 | be './node_modules/phantomcss'. 101 | */ 102 | libraryRoot: './modules/PhantomCSS', 103 | 104 | screenshotRoot: './screenshots', 105 | 106 | /* 107 | By default, failure images are put in the './failures' folder. 108 | If failedComparisonsRoot is set to false a separate folder will 109 | not be created but failure images can still be found alongside 110 | the original and new images. 111 | */ 112 | failedComparisonsRoot: './failures', 113 | 114 | /* 115 | Remove results directory tree after run. Use in conjunction 116 | with failedComparisonsRoot to see failed comparisons. 117 | */ 118 | cleanupComparisonImages: true, 119 | 120 | /* 121 | A reference to a particular Casper instance. Required for SlimerJS. 122 | */ 123 | casper: specific_instance_of_casper, 124 | 125 | /* 126 | You might want to keep master/baseline images in a completely 127 | different folder to the diffs/failures. Useful when working 128 | with version control systems. By default this resolves to the 129 | screenshotRoot folder. 130 | */ 131 | comparisonResultRoot: './results', 132 | 133 | /* 134 | Don't add count number to images. If set to false, a filename is 135 | required when capturing screenshots. 136 | */ 137 | addIteratorToImage: false, 138 | 139 | /* 140 | Don't add label to generated failure image 141 | */ 142 | addLabelToFailedImage: false, 143 | 144 | /* 145 | Mismatch tolerance defaults to 0.05%. Increasing this value 146 | will decrease test coverage 147 | */ 148 | mismatchTolerance: 0.05, 149 | 150 | /* 151 | Callbacks for your specific integration 152 | */ 153 | onFail: function(test){ console.log(test.filename, test.mismatch); }, 154 | 155 | onPass: function(test){ console.log(test.filename); }, 156 | 157 | /* 158 | Called when creating new baseline images 159 | */ 160 | onNewImage: function(){ console.log(test.filename); }, 161 | 162 | onTimeout: function(){ console.log(test.filename); }, 163 | 164 | onComplete: function(allTests, noOfFails, noOfErrors){ 165 | allTests.forEach(function(test){ 166 | if(test.fail){ 167 | console.log(test.filename, test.mismatch); 168 | } 169 | }); 170 | }, 171 | 172 | onCaptureFail: function(ex, target) { console.log('Capture of ' + target + ' failed due to ' + ex.message); } 173 | 174 | /* 175 | Change the output screenshot filenames for your specific 176 | integration 177 | */ 178 | fileNameGetter: function(root,filename){ 179 | // globally override output filename 180 | // files must exist under root 181 | // and use the .diff convention 182 | var name = root+'/somewhere/'+filename; 183 | if(fs.isFile(name+'.png')){ 184 | return name+'.diff.png'; 185 | } else { 186 | return name+'.png'; 187 | } 188 | }, 189 | 190 | /* 191 | Prefix the screenshot number to the filename, instead of suffixing it 192 | */ 193 | prefixCount: true, 194 | 195 | /* 196 | Output styles for image failure outputs generated by Resemble.js 197 | */ 198 | outputSettings: { 199 | errorColor: { 200 | red: 255, 201 | green: 255, 202 | blue: 0 203 | }, 204 | errorType: 'movement', 205 | transparency: 0.3 206 | }, 207 | 208 | /* 209 | Rebase is useful when you want to create new baseline 210 | images without manually deleting the files 211 | casperjs demo/test.js --rebase 212 | */ 213 | rebase: casper.cli.get("rebase"), 214 | 215 | /* 216 | If true, test will fail when captures fail (e.g. no element matching selector). 217 | */ 218 | failOnCaptureError: false 219 | }); 220 | 221 | /* 222 | Turn off CSS transitions and jQuery animations 223 | */ 224 | phantomcss.turnOffAnimations(); 225 | ``` 226 | 227 | ### Don't like pink? 228 | 229 | ![A failed visual regression test, yellow areas show where the icon has enlarged and pushed other elements down.](https://raw.github.com/Huddle/PhantomCSS/master/readme_assets/differentcolour.png "Failed visual regression test") 230 | 231 | ```javascript 232 | phantomcss.init({ 233 | /* 234 | Output styles for image failure outputs generated by Resemble.js 235 | */ 236 | outputSettings: { 237 | 238 | /* 239 | Error pixel color, RGB, anything you want, 240 | though bright and ugly works best! 241 | */ 242 | errorColor: { 243 | red: 255, 244 | green: 255, 245 | blue: 0 246 | }, 247 | 248 | /* 249 | ErrorType values include 'flat', or 'movement'. 250 | The latter merges error color with base image 251 | which makes it a little easier to spot movement. 252 | */ 253 | errorType: 'movement', 254 | 255 | /* 256 | Fade unchanged areas to make changed areas more apparent. 257 | */ 258 | transparency: 0.3 259 | } 260 | }); 261 | ``` 262 | 263 | ### There are different ways to take a screenshot 264 | 265 | ```javascript 266 | var delay = 10; 267 | var hideElements = 'input[type=file]'; 268 | var screenshotName = 'the_dialog' 269 | 270 | phantomcss.screenshot( "#CSS .selector", screenshotName); 271 | 272 | // phantomcss.screenshot({ 273 | // 'Screenshot 1 File name': {selector: '.screenshot1', ignore: '.selector'}, 274 | // 'Screenshot 2 File name': '#screenshot2' 275 | // }); 276 | // phantomcss.screenshot( "#CSS .selector" ); 277 | // phantomcss.screenshot( "#CSS .selector", delay, hideElements, screenshotName); 278 | 279 | // phantomcss.screenshot({ 280 | // top: 100, 281 | // left: 100, 282 | // width: 500, 283 | // height: 400 284 | // }, screenshotName); 285 | ``` 286 | 287 | ### Compare the images when and how you want 288 | 289 | ```javascript 290 | /* 291 | String is converted into a Regular expression that matches on full image path 292 | */ 293 | phantomcss.compareAll('exclude.test'); 294 | 295 | // phantomcss.compareMatched('include.test', 'exclude.test'); 296 | // phantomcss.compareMatched( new RegExp('include.test'), new RegExp('exclude.test')); 297 | 298 | /* 299 | Compare image diffs generated in this test run only 300 | */ 301 | // phantomcss.compareSession(); 302 | 303 | /* 304 | Explicitly define what files you want to compare 305 | */ 306 | // phantomcss.compareExplicit(['/dialog.diff.png', '/header.diff.png']); 307 | 308 | /* 309 | Get a list of image diffs generated in this test run 310 | */ 311 | // phantomcss.getCreatedDiffFiles(); 312 | 313 | /* 314 | Compare any two images, and wait for the results to complete 315 | */ 316 | // phantomcss.compareFiles(baseFile, diffFile); 317 | // phantomcss.waitForTests(); 318 | 319 | ``` 320 | 321 | ### Best Practices 322 | 323 | ##### Name your screenshots! 324 | 325 | By default PhantomCSS creates a file called screenshot_0.png, not very helpful. You can name your screenshot by passing a string to either the second or forth parameter. 326 | 327 | ```javascript 328 | var delay, hideElementsSelector; 329 | 330 | phantomcss.screenshot("#feedback-form", delay, hideElementsSelector, "Responsive Feedback Form"); 331 | 332 | phantomcss.screenshot("#feedback-form", "Responsive Feedback Form"); 333 | 334 | ``` 335 | 336 | Perhaps a better way is to use the ‘fileNameGetter’ callback property on the ‘init’ method. This does involve having a bit more structure around your tests. See: https://github.com/Huddle/PhantomFlow/blob/master/lib/phantomCSSAdaptor.js#L41 337 | 338 | ##### CSS3 selectors for testing 339 | 340 | Try not to use complex CSS3 selectors for asserting or creating screenshots. In the same way that CSS should be written with good content/container separation, so should your test selectors be agnostic of location/context. This might mean you need to add more ID's or data- attributes into your mark-up, but it's worth it, your tests will be more stable and more explicit. 341 | This is not a good idea: 342 | 343 | ```javascript 344 | phantomcss.screenshot("#sidebar li:nth-child(3) > div form"); 345 | ``` 346 | 347 | But this is: 348 | 349 | ```javascript 350 | phantomcss.screenshot("#feedback-form"); 351 | ``` 352 | 353 | ##### PhantomCSS should not be used to replace functional tests 354 | 355 | If you needed functional tests before, then you still need them. Automated visual regression testing gives us coverage of CSS and design in a way we didn't have before, but that doesn't mean that conventional test assertions are now invalid. Feedback time is crucial with test automation, the longer it takes the easier it is to ignore; the easier it is to ignore the sooner trust is lost from the team. Unfortunately comparing images is not, and never will be as fast as simple DOM assertion. 356 | 357 | ##### Don't try to test all the visuals 358 | 359 | I'd argue this applies to all automated testing approaches. As a rule, try to maximise coverage with fewer tests. This is a difficult balancing act because granular feedback/reporting is very important for debugging and build analysis. Testing many things in one assert/screenshot might tell you there is a problem, but makes it harder to get to the root of the bug. As a CSS/HTML Dev you'll know what components are more fragile than others, which are reused and which aren't, concentrate your visual tests on these areas. 360 | 361 | ##### Full page screenshots are a bad idea 362 | 363 | If you try to test too much in one screenshot then you could end up with lots of failing tests every time someone makes a small change. Say you've set up full-page visual regression tests for your 50 page website, and someone adds 2px padding to the footer - that’s 50 failed tests because of one change. It's better to test UI components individually; in this example the footer could have its own test. 364 | There is also a technical problem with this approach, the larger the image, the longer it takes to process. An added pixel padding on the page body will offset everything, at best you'll have a sea of pink in the failed diff, at worse you'll get a TIMEOUT because it took too long to analyse. 365 | 366 | ##### Scaling visual regression testing within a large team 367 | 368 | Scaling your test suite for many contributors may not be easy. [Resemble.js](http://huddle.github.com/Resemble.js/) (the core analysis engine of PhantomCSS) tries to consider image differences caused by different operating systems and graphics cards, but it's only so good, you are likely to see problems as more people contribute baseline screenshots. You can mitigate this by hiding problematic elements such as select elements, file upload inputs etc. as so. 369 | 370 | ```javascript 371 | phantomcss.screenshot("#feedback-form", undefined, 'input[type=file]'); 372 | ``` 373 | 374 | Below is an example of a false-negative caused by antialiasing differences on different machines. How can we solve this? **Contributions welcome!** 375 | 376 | ![Three images: baseline, latests and diff where antialiasing has caused the failed diff](https://raw.github.com/Huddle/PhantomCSS/master/readme_assets/false-negative.png "A False-negative?") 377 | 378 | ##### Scaling visual regression testing with Git 379 | 380 | If you're using a version control system like Git to store the baseline screenshots the repository size becomes increasingly relevant as your test suite grows. I'd recommend using a tool like https://github.com/joeyh/git-annex or https://github.com/schacon/git-media to store the images outside of the repo. 381 | 382 | ### ...You might also be interested in 383 | 384 | **[PhantomFlow](https://github.com/Huddle/PhantomFlow)** and **[grunt-phantomflow](https://github.com/Huddle/grunt-testflow)** wrap PhantomCSS and provides an experimental way of describing and visualising user flows through tests with CasperJS. As well as providing a terse readable structure for UI testing, it also produces intriguing graph visualisations that can be used to present PhantomCSS screenshots and failed diffs. We're actively using it at Huddle and it's changing the way we think about UI for the better. 385 | 386 | Also, take a look at [PhantomXHR](http://github.com/Huddle/PhantomXHR) for stubbing and mocking XHR requests. Isolated UI testing IS THE FUTURE! 387 | 388 | ### Why is this project no longer maintained 389 | 390 | The introduction of [headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome) has simply meant that PhantomJS is no longer the best tool for running browser tests. Huddle is making efforts to move away from PhantomJS based testing, largely to gain better coverage of new browser features such as CSS grid. Interestingly there still doesn't seem to be a straight replacement for PhantomCSS for Chrome, perhaps because it is now far easier to roll-your-own VRT suite. The Huddle development team is now actively looking into using Docker containers for running Mocha/Chai test suites against headless Chrome, using [Resemblejs](https://github.com/Huddle/Resemble.js) directly in NodeJS for image comparison. 391 | 392 | ### Huddle Careers 393 | 394 | Huddle strongly believe in innovation and give you 20% of work time to spend on innovative projects of your choosing. 395 | 396 | If you like what you see and would like to work on this kind of stuff for a job then get in touch. 397 | 398 | Visit http://www.huddle.com/careers for open vacancies now, or register your interest for the future. 399 | 400 | -------------------------------------- 401 | 402 | PhantomCSS was created by [James Cryer](http://github.com/jamescryer) and the Huddle development team. 403 | -------------------------------------------------------------------------------- /casperjs.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | set CASPER_PATH=%~dp0node_modules\casperjs 3 | set CASPER_BIN=%CASPER_PATH%\bin\ 4 | set PHANTOMJS=%~dp0node_modules\phantomjs-prebuilt\lib\phantom\bin\phantomjs.exe 5 | set ARGV=%* 6 | set IS_SLIMERJS="false" 7 | 8 | for %%a in (%*) do ( 9 | if %%a equ slimerjs ( 10 | set IS_SLIMERJS="true" 11 | ) 12 | ) 13 | 14 | if %IS_SLIMERJS% equ "false" ( 15 | call "%PHANTOMJS%" "%CASPER_BIN%bootstrap.js" --casper-path="%CASPER_PATH%" --cli %ARGV% 16 | ) else ( 17 | call slimerjs "%CASPER_BIN%bootstrap.js" --casper-path="%CASPER_PATH%" --cli %ARGV% 18 | ) -------------------------------------------------------------------------------- /demo/coffeemachine.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PhantomCSS Demo 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 |
23 |

Would you like a coffee?

24 | 25 | Go to the Coffee Machine 26 | 27 | 28 | 72 |
73 | 74 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /demo/testsuite.js: -------------------------------------------------------------------------------- 1 | /* 2 | Require and initialise PhantomCSS module 3 | Paths are relative to CasperJs directory 4 | */ 5 | 6 | var fs = require( 'fs' ); 7 | var path = fs.absolute( fs.workingDirectory + '/phantomcss.js' ); 8 | var phantomcss = require( path ); 9 | var server = require('webserver').create(); 10 | 11 | var html = fs.read( fs.absolute( fs.workingDirectory + '/demo/coffeemachine.html' )); 12 | 13 | server.listen(8080,function(req,res){ 14 | res.statusCode = 200; 15 | res.headers = { 16 | 'Cache': 'no-cache', 17 | 'Content-Type': 'text/html;charset=utf-8' 18 | }; 19 | res.write(html); 20 | res.close(); 21 | }); 22 | 23 | 24 | casper.test.begin( 'Coffee machine visual tests', function ( test ) { 25 | 26 | phantomcss.init( { 27 | rebase: casper.cli.get( "rebase" ), 28 | // SlimerJS needs explicit knowledge of this Casper, and lots of absolute paths 29 | casper: casper, 30 | libraryRoot: fs.absolute( fs.workingDirectory + '' ), 31 | screenshotRoot: fs.absolute( fs.workingDirectory + '/screenshots' ), 32 | failedComparisonsRoot: fs.absolute( fs.workingDirectory + '/demo/failures' ), 33 | addLabelToFailedImage: false, 34 | /* 35 | screenshotRoot: '/screenshots', 36 | failedComparisonsRoot: '/failures' 37 | casper: specific_instance_of_casper, 38 | libraryRoot: '/phantomcss', 39 | fileNameGetter: function overide_file_naming(){}, 40 | onPass: function passCallback(){}, 41 | onFail: function failCallback(){}, 42 | onTimeout: function timeoutCallback(){}, 43 | onComplete: function completeCallback(){}, 44 | hideElements: '#thing.selector', 45 | addLabelToFailedImage: true, 46 | outputSettings: { 47 | errorColor: { 48 | red: 255, 49 | green: 255, 50 | blue: 0 51 | }, 52 | errorType: 'movement', 53 | transparency: 0.3 54 | }*/ 55 | } ); 56 | 57 | casper.on( 'remote.message', function ( msg ) { 58 | this.echo( msg ); 59 | } ); 60 | 61 | casper.on( 'error', function ( err ) { 62 | this.die( "PhantomJS has errored: " + err ); 63 | } ); 64 | 65 | casper.on( 'resource.error', function ( err ) { 66 | casper.log( 'Resource load error: ' + err, 'warning' ); 67 | } ); 68 | /* 69 | The test scenario 70 | */ 71 | 72 | casper.start( 'http://localhost:8080' ); 73 | 74 | casper.viewport( 1024, 768 ); 75 | 76 | casper.then( function () { 77 | phantomcss.screenshot( '#coffee-machine-wrapper', 'open coffee machine button' ); 78 | } ); 79 | 80 | casper.then( function () { 81 | casper.click( '#coffee-machine-button' ); 82 | 83 | // wait for modal to fade-in 84 | casper.waitForSelector( '#myModal:not([style*="display: none"])', 85 | function success() { 86 | phantomcss.screenshot( '#myModal', 'coffee machine dialog' ); 87 | }, 88 | function timeout() { 89 | casper.test.fail( 'Should see coffee machine' ); 90 | } 91 | ); 92 | } ); 93 | 94 | casper.then( function () { 95 | casper.click( '#cappuccino-button' ); 96 | phantomcss.screenshot( '#myModal', 'cappuccino success' ); 97 | } ); 98 | 99 | casper.then( function () { 100 | casper.click( '#close' ); 101 | 102 | // wait for modal to fade-out 103 | casper.waitForSelector( '#myModal[style*="display: none"]', 104 | function success() { 105 | phantomcss.screenshot( { 106 | 'Coffee machine close success': { 107 | selector: '#coffee-machine-wrapper', 108 | ignore: '.selector' 109 | }, 110 | 'Coffee machine button success': '#coffee-machine-button' 111 | } ); 112 | }, 113 | function timeout() { 114 | casper.test.fail( 'Should be able to walk away from the coffee machine' ); 115 | } 116 | ); 117 | } ); 118 | 119 | casper.then( function now_check_the_screenshots() { 120 | // compare screenshots 121 | phantomcss.compareAll(); 122 | } ); 123 | 124 | /* 125 | Casper runs tests 126 | */ 127 | casper.run( function () { 128 | console.log( '\nTHE END.' ); 129 | // phantomcss.getExitStatus() // pass or fail? 130 | casper.test.done(); 131 | } ); 132 | } ); 133 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomcss", 3 | "version": "1.6.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "ansi-regex": { 13 | "version": "2.1.1", 14 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 15 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 16 | }, 17 | "aproba": { 18 | "version": "1.2.0", 19 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 20 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 21 | }, 22 | "are-we-there-yet": { 23 | "version": "1.1.5", 24 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 25 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 26 | "requires": { 27 | "delegates": "^1.0.0", 28 | "readable-stream": "^2.0.6" 29 | } 30 | }, 31 | "balanced-match": { 32 | "version": "1.0.0", 33 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 34 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 35 | }, 36 | "brace-expansion": { 37 | "version": "1.1.11", 38 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 39 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 40 | "requires": { 41 | "balanced-match": "^1.0.0", 42 | "concat-map": "0.0.1" 43 | } 44 | }, 45 | "canvas": { 46 | "version": "2.2.0", 47 | "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.2.0.tgz", 48 | "integrity": "sha512-4blMi2I2DuHh9UZNrmvU0hyY4dZJFOjNuqaZpI/66pKCyX1HPstvK+f2fIdc+NaF8b6wiuhvwXEFNkm7jIKYSA==", 49 | "requires": { 50 | "nan": "^2.11.1", 51 | "node-pre-gyp": "^0.11.0" 52 | } 53 | }, 54 | "casperjs": { 55 | "version": "1.1.4", 56 | "resolved": "https://registry.npmjs.org/casperjs/-/casperjs-1.1.4.tgz", 57 | "integrity": "sha1-6wH07YWsUgqPTZMrTap00+d7x0Y=" 58 | }, 59 | "chownr": { 60 | "version": "1.1.1", 61 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", 62 | "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" 63 | }, 64 | "code-point-at": { 65 | "version": "1.1.0", 66 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 67 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 68 | }, 69 | "concat-map": { 70 | "version": "0.0.1", 71 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 72 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 73 | }, 74 | "console-control-strings": { 75 | "version": "1.1.0", 76 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 77 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 78 | }, 79 | "core-util-is": { 80 | "version": "1.0.2", 81 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 82 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 83 | }, 84 | "debug": { 85 | "version": "2.6.9", 86 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 87 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 88 | "requires": { 89 | "ms": "2.0.0" 90 | } 91 | }, 92 | "deep-extend": { 93 | "version": "0.6.0", 94 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 95 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 96 | }, 97 | "delegates": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 100 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 101 | }, 102 | "detect-libc": { 103 | "version": "1.0.3", 104 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 105 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 106 | }, 107 | "es6-promise": { 108 | "version": "4.0.5", 109 | "resolved": false, 110 | "integrity": "sha1-eILzCt3lskDM+n99eMVIMwlRrkI=" 111 | }, 112 | "fs-extra": { 113 | "version": "1.0.0", 114 | "resolved": false, 115 | "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", 116 | "requires": { 117 | "graceful-fs": "^4.1.2", 118 | "jsonfile": "^2.1.0", 119 | "klaw": "^1.0.0" 120 | } 121 | }, 122 | "fs-minipass": { 123 | "version": "1.2.5", 124 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", 125 | "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", 126 | "requires": { 127 | "minipass": "^2.2.1" 128 | } 129 | }, 130 | "fs.realpath": { 131 | "version": "1.0.0", 132 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 133 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 134 | }, 135 | "gauge": { 136 | "version": "2.7.4", 137 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 138 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 139 | "requires": { 140 | "aproba": "^1.0.3", 141 | "console-control-strings": "^1.0.0", 142 | "has-unicode": "^2.0.0", 143 | "object-assign": "^4.1.0", 144 | "signal-exit": "^3.0.0", 145 | "string-width": "^1.0.1", 146 | "strip-ansi": "^3.0.1", 147 | "wide-align": "^1.1.0" 148 | } 149 | }, 150 | "glob": { 151 | "version": "7.1.3", 152 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 153 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 154 | "requires": { 155 | "fs.realpath": "^1.0.0", 156 | "inflight": "^1.0.4", 157 | "inherits": "2", 158 | "minimatch": "^3.0.4", 159 | "once": "^1.3.0", 160 | "path-is-absolute": "^1.0.0" 161 | } 162 | }, 163 | "graceful-fs": { 164 | "version": "4.1.11", 165 | "resolved": false, 166 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 167 | }, 168 | "has-unicode": { 169 | "version": "2.0.1", 170 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 171 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 172 | }, 173 | "hasha": { 174 | "version": "2.2.0", 175 | "resolved": false, 176 | "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", 177 | "requires": { 178 | "is-stream": "^1.0.1", 179 | "pinkie-promise": "^2.0.0" 180 | } 181 | }, 182 | "iconv-lite": { 183 | "version": "0.4.24", 184 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 185 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 186 | "requires": { 187 | "safer-buffer": ">= 2.1.2 < 3" 188 | } 189 | }, 190 | "ignore-walk": { 191 | "version": "3.0.1", 192 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", 193 | "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", 194 | "requires": { 195 | "minimatch": "^3.0.4" 196 | } 197 | }, 198 | "inflight": { 199 | "version": "1.0.6", 200 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 201 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 202 | "requires": { 203 | "once": "^1.3.0", 204 | "wrappy": "1" 205 | } 206 | }, 207 | "inherits": { 208 | "version": "2.0.3", 209 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 210 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 211 | }, 212 | "ini": { 213 | "version": "1.3.5", 214 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 215 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 216 | }, 217 | "is-fullwidth-code-point": { 218 | "version": "1.0.0", 219 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 220 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 221 | "requires": { 222 | "number-is-nan": "^1.0.0" 223 | } 224 | }, 225 | "is-stream": { 226 | "version": "1.1.0", 227 | "resolved": false, 228 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 229 | }, 230 | "isarray": { 231 | "version": "1.0.0", 232 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 233 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 234 | }, 235 | "isexe": { 236 | "version": "1.1.2", 237 | "resolved": false, 238 | "integrity": "sha1-NvPiLmB1CSD15yQaR2qMakInWtA=" 239 | }, 240 | "jsonfile": { 241 | "version": "2.4.0", 242 | "resolved": false, 243 | "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", 244 | "requires": { 245 | "graceful-fs": "^4.1.6" 246 | } 247 | }, 248 | "kew": { 249 | "version": "0.7.0", 250 | "resolved": false, 251 | "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=" 252 | }, 253 | "klaw": { 254 | "version": "1.3.1", 255 | "resolved": false, 256 | "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", 257 | "requires": { 258 | "graceful-fs": "^4.1.9" 259 | } 260 | }, 261 | "minimatch": { 262 | "version": "3.0.4", 263 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 264 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 265 | "requires": { 266 | "brace-expansion": "^1.1.7" 267 | } 268 | }, 269 | "minimist": { 270 | "version": "0.0.8", 271 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 272 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 273 | }, 274 | "minipass": { 275 | "version": "2.3.5", 276 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", 277 | "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", 278 | "requires": { 279 | "safe-buffer": "^5.1.2", 280 | "yallist": "^3.0.0" 281 | } 282 | }, 283 | "minizlib": { 284 | "version": "1.1.1", 285 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", 286 | "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", 287 | "requires": { 288 | "minipass": "^2.2.1" 289 | } 290 | }, 291 | "mkdirp": { 292 | "version": "0.5.1", 293 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 294 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 295 | "requires": { 296 | "minimist": "0.0.8" 297 | } 298 | }, 299 | "ms": { 300 | "version": "2.0.0", 301 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 302 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 303 | }, 304 | "nan": { 305 | "version": "2.11.1", 306 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", 307 | "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==" 308 | }, 309 | "needle": { 310 | "version": "2.2.4", 311 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", 312 | "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", 313 | "requires": { 314 | "debug": "^2.1.2", 315 | "iconv-lite": "^0.4.4", 316 | "sax": "^1.2.4" 317 | } 318 | }, 319 | "node-pre-gyp": { 320 | "version": "0.11.0", 321 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", 322 | "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", 323 | "requires": { 324 | "detect-libc": "^1.0.2", 325 | "mkdirp": "^0.5.1", 326 | "needle": "^2.2.1", 327 | "nopt": "^4.0.1", 328 | "npm-packlist": "^1.1.6", 329 | "npmlog": "^4.0.2", 330 | "rc": "^1.2.7", 331 | "rimraf": "^2.6.1", 332 | "semver": "^5.3.0", 333 | "tar": "^4" 334 | } 335 | }, 336 | "nopt": { 337 | "version": "4.0.1", 338 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", 339 | "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", 340 | "requires": { 341 | "abbrev": "1", 342 | "osenv": "^0.1.4" 343 | } 344 | }, 345 | "npm-bundled": { 346 | "version": "1.0.5", 347 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", 348 | "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" 349 | }, 350 | "npm-packlist": { 351 | "version": "1.1.12", 352 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", 353 | "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", 354 | "requires": { 355 | "ignore-walk": "^3.0.1", 356 | "npm-bundled": "^1.0.1" 357 | } 358 | }, 359 | "npmlog": { 360 | "version": "4.1.2", 361 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 362 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 363 | "requires": { 364 | "are-we-there-yet": "~1.1.2", 365 | "console-control-strings": "~1.1.0", 366 | "gauge": "~2.7.3", 367 | "set-blocking": "~2.0.0" 368 | } 369 | }, 370 | "number-is-nan": { 371 | "version": "1.0.1", 372 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 373 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 374 | }, 375 | "object-assign": { 376 | "version": "4.1.1", 377 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 378 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 379 | }, 380 | "once": { 381 | "version": "1.4.0", 382 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 383 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 384 | "requires": { 385 | "wrappy": "1" 386 | } 387 | }, 388 | "os-homedir": { 389 | "version": "1.0.2", 390 | "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 391 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 392 | }, 393 | "os-tmpdir": { 394 | "version": "1.0.2", 395 | "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 396 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 397 | }, 398 | "osenv": { 399 | "version": "0.1.5", 400 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 401 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 402 | "requires": { 403 | "os-homedir": "^1.0.0", 404 | "os-tmpdir": "^1.0.0" 405 | } 406 | }, 407 | "path-is-absolute": { 408 | "version": "1.0.1", 409 | "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 410 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 411 | }, 412 | "phantomjs-prebuilt": { 413 | "version": "2.1.16", 414 | "resolved": "https://proget.huddle.local/npm/npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", 415 | "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", 416 | "requires": { 417 | "es6-promise": "^4.0.3", 418 | "extract-zip": "^1.6.5", 419 | "fs-extra": "^1.0.0", 420 | "hasha": "^2.2.0", 421 | "kew": "^0.7.0", 422 | "progress": "^1.1.8", 423 | "request": "^2.81.0", 424 | "request-progress": "^2.0.1", 425 | "which": "^1.2.10" 426 | }, 427 | "dependencies": { 428 | "assert-plus": { 429 | "version": "1.0.0", 430 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 431 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 432 | }, 433 | "aws-sign2": { 434 | "version": "0.7.0", 435 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 436 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 437 | }, 438 | "boom": { 439 | "version": "4.3.1", 440 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", 441 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", 442 | "requires": { 443 | "hoek": "4.x.x" 444 | } 445 | }, 446 | "caseless": { 447 | "version": "0.12.0", 448 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 449 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 450 | }, 451 | "concat-stream": { 452 | "version": "1.6.0", 453 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", 454 | "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", 455 | "requires": { 456 | "inherits": "^2.0.3", 457 | "readable-stream": "^2.2.2" 458 | } 459 | }, 460 | "cryptiles": { 461 | "version": "3.1.2", 462 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", 463 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", 464 | "requires": { 465 | "boom": "5.x.x" 466 | }, 467 | "dependencies": { 468 | "boom": { 469 | "version": "5.2.0", 470 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 471 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 472 | "requires": { 473 | "hoek": "4.x.x" 474 | } 475 | } 476 | } 477 | }, 478 | "debug": { 479 | "version": "2.6.9", 480 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 481 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 482 | "requires": { 483 | "ms": "2.0.0" 484 | } 485 | }, 486 | "extend": { 487 | "version": "3.0.1", 488 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 489 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 490 | }, 491 | "extract-zip": { 492 | "version": "1.6.6", 493 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", 494 | "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", 495 | "requires": { 496 | "concat-stream": "1.6.0", 497 | "debug": "2.6.9" 498 | } 499 | }, 500 | "form-data": { 501 | "version": "2.3.1", 502 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", 503 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", 504 | "requires": { 505 | "mime-types": "^2.1.12" 506 | } 507 | }, 508 | "har-validator": { 509 | "version": "5.0.3", 510 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 511 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=" 512 | }, 513 | "hawk": { 514 | "version": "6.0.2", 515 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", 516 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", 517 | "requires": { 518 | "boom": "4.x.x", 519 | "cryptiles": "3.x.x", 520 | "hoek": "4.x.x", 521 | "sntp": "2.x.x" 522 | } 523 | }, 524 | "hoek": { 525 | "version": "4.2.0", 526 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", 527 | "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" 528 | }, 529 | "http-signature": { 530 | "version": "1.2.0", 531 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 532 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 533 | "requires": { 534 | "assert-plus": "^1.0.0" 535 | } 536 | }, 537 | "mime-db": { 538 | "version": "1.30.0", 539 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 540 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 541 | }, 542 | "mime-types": { 543 | "version": "2.1.17", 544 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 545 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 546 | "requires": { 547 | "mime-db": "~1.30.0" 548 | } 549 | }, 550 | "qs": { 551 | "version": "6.5.1", 552 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 553 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 554 | }, 555 | "readable-stream": { 556 | "version": "2.3.3", 557 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 558 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 559 | "requires": { 560 | "core-util-is": "~1.0.0", 561 | "inherits": "~2.0.3", 562 | "isarray": "~1.0.0", 563 | "safe-buffer": "~5.1.1", 564 | "string_decoder": "~1.0.3", 565 | "util-deprecate": "~1.0.1" 566 | } 567 | }, 568 | "request": { 569 | "version": "2.83.0", 570 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", 571 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", 572 | "requires": { 573 | "aws-sign2": "~0.7.0", 574 | "caseless": "~0.12.0", 575 | "extend": "~3.0.1", 576 | "form-data": "~2.3.1", 577 | "har-validator": "~5.0.3", 578 | "hawk": "~6.0.2", 579 | "http-signature": "~1.2.0", 580 | "mime-types": "~2.1.17", 581 | "qs": "~6.5.1", 582 | "safe-buffer": "^5.1.1", 583 | "tough-cookie": "~2.3.3", 584 | "tunnel-agent": "^0.6.0", 585 | "uuid": "^3.1.0" 586 | } 587 | }, 588 | "sntp": { 589 | "version": "2.1.0", 590 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", 591 | "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", 592 | "requires": { 593 | "hoek": "4.x.x" 594 | } 595 | }, 596 | "string_decoder": { 597 | "version": "1.0.3", 598 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 599 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 600 | "requires": { 601 | "safe-buffer": "~5.1.0" 602 | } 603 | }, 604 | "tough-cookie": { 605 | "version": "2.3.3", 606 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", 607 | "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=" 608 | }, 609 | "tunnel-agent": { 610 | "version": "0.6.0", 611 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 612 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 613 | "requires": { 614 | "safe-buffer": "^5.0.1" 615 | } 616 | }, 617 | "uuid": { 618 | "version": "3.1.0", 619 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 620 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 621 | } 622 | } 623 | }, 624 | "pinkie": { 625 | "version": "2.0.4", 626 | "resolved": false, 627 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" 628 | }, 629 | "pinkie-promise": { 630 | "version": "2.0.1", 631 | "resolved": false, 632 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 633 | "requires": { 634 | "pinkie": "^2.0.0" 635 | } 636 | }, 637 | "process-nextick-args": { 638 | "version": "2.0.0", 639 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 640 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 641 | }, 642 | "progress": { 643 | "version": "1.1.8", 644 | "resolved": false, 645 | "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" 646 | }, 647 | "rc": { 648 | "version": "1.2.8", 649 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 650 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 651 | "requires": { 652 | "deep-extend": "^0.6.0", 653 | "ini": "~1.3.0", 654 | "minimist": "^1.2.0", 655 | "strip-json-comments": "~2.0.1" 656 | }, 657 | "dependencies": { 658 | "minimist": { 659 | "version": "1.2.0", 660 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 661 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 662 | } 663 | } 664 | }, 665 | "readable-stream": { 666 | "version": "2.3.6", 667 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 668 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 669 | "requires": { 670 | "core-util-is": "~1.0.0", 671 | "inherits": "~2.0.3", 672 | "isarray": "~1.0.0", 673 | "process-nextick-args": "~2.0.0", 674 | "safe-buffer": "~5.1.1", 675 | "string_decoder": "~1.1.1", 676 | "util-deprecate": "~1.0.1" 677 | } 678 | }, 679 | "request-progress": { 680 | "version": "2.0.1", 681 | "resolved": false, 682 | "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", 683 | "requires": { 684 | "throttleit": "^1.0.0" 685 | } 686 | }, 687 | "resemblejs": { 688 | "version": "3.0.0", 689 | "resolved": "https://registry.npmjs.org/resemblejs/-/resemblejs-3.0.0.tgz", 690 | "integrity": "sha512-CLvvcUxacYyLcIRjWH4K5j8VvSngAX1puDnVeVolUm7LPlOdtSpwyPxtjh+41QKapg5FYyBvCoBS3cCdRgo/yg==", 691 | "requires": { 692 | "canvas": "2.2.0" 693 | } 694 | }, 695 | "rimraf": { 696 | "version": "2.6.2", 697 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 698 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 699 | "requires": { 700 | "glob": "^7.0.5" 701 | } 702 | }, 703 | "safe-buffer": { 704 | "version": "5.1.2", 705 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 706 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 707 | }, 708 | "safer-buffer": { 709 | "version": "2.1.2", 710 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 711 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 712 | }, 713 | "sax": { 714 | "version": "1.2.4", 715 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 716 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 717 | }, 718 | "semver": { 719 | "version": "5.6.0", 720 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 721 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 722 | }, 723 | "set-blocking": { 724 | "version": "2.0.0", 725 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 726 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 727 | }, 728 | "signal-exit": { 729 | "version": "3.0.2", 730 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 731 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 732 | }, 733 | "string-width": { 734 | "version": "1.0.2", 735 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 736 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 737 | "requires": { 738 | "code-point-at": "^1.0.0", 739 | "is-fullwidth-code-point": "^1.0.0", 740 | "strip-ansi": "^3.0.0" 741 | } 742 | }, 743 | "string_decoder": { 744 | "version": "1.1.1", 745 | "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 746 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 747 | "requires": { 748 | "safe-buffer": "~5.1.0" 749 | } 750 | }, 751 | "strip-ansi": { 752 | "version": "3.0.1", 753 | "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 754 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 755 | "requires": { 756 | "ansi-regex": "^2.0.0" 757 | } 758 | }, 759 | "strip-json-comments": { 760 | "version": "2.0.1", 761 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 762 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 763 | }, 764 | "tar": { 765 | "version": "4.4.8", 766 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", 767 | "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", 768 | "requires": { 769 | "chownr": "^1.1.1", 770 | "fs-minipass": "^1.2.5", 771 | "minipass": "^2.3.4", 772 | "minizlib": "^1.1.1", 773 | "mkdirp": "^0.5.0", 774 | "safe-buffer": "^5.1.2", 775 | "yallist": "^3.0.2" 776 | } 777 | }, 778 | "throttleit": { 779 | "version": "1.0.0", 780 | "resolved": false, 781 | "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" 782 | }, 783 | "util-deprecate": { 784 | "version": "1.0.2", 785 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 786 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 787 | }, 788 | "which": { 789 | "version": "1.2.12", 790 | "resolved": false, 791 | "integrity": "sha1-3me15FAmnxlJCe8j7OTr5Bb6EZI=", 792 | "requires": { 793 | "isexe": "^1.1.1" 794 | } 795 | }, 796 | "wide-align": { 797 | "version": "1.1.3", 798 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 799 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 800 | "requires": { 801 | "string-width": "^1.0.2 || 2" 802 | } 803 | }, 804 | "wrappy": { 805 | "version": "1.0.2", 806 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 807 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 808 | }, 809 | "yallist": { 810 | "version": "3.0.3", 811 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", 812 | "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" 813 | } 814 | } 815 | } 816 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomcss", 3 | "version": "1.6.0", 4 | "description": "A CasperJS module for automating visual regression testing of Web apps, live style guides and responsive layouts.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Huddle/PhantomCSS.git" 8 | }, 9 | "main": "phantomcss.js", 10 | "keywords": [ 11 | "css", 12 | "phantomjs", 13 | "casperjs", 14 | "testing", 15 | "visual regression", 16 | "responsive" 17 | ], 18 | "author": "James Cryer / Huddle", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Huddle/PhantomCSS/issues" 22 | }, 23 | "dependencies": { 24 | "casperjs": "^1.1.4", 25 | "phantomjs-prebuilt": "^2.1.16", 26 | "resemblejs": "^3.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phantomcss.js: -------------------------------------------------------------------------------- 1 | /* 2 | James Cryer / Huddle / 2016 3 | https://github.com/Huddle/PhantomCSS 4 | http://tldr.huddle.com/blog/css-testing/ 5 | */ 6 | 7 | var fs = require( 'fs' ); 8 | 9 | var _src = '.' + fs.separator + 'screenshots'; 10 | var _results; // for backwards compatibility results and src are the same - but you can change it! 11 | var _failures = '.' + fs.separator + 'failures'; 12 | 13 | var _count = 0; 14 | var exitStatus; 15 | var _hideElements; 16 | var _waitTimeout = 60000; 17 | var _addLabelToFailedImage = true; 18 | var _mismatchTolerance = 0.05; 19 | var _resembleOutputSettings = {}; 20 | var _cleanupComparisonImages = false; 21 | var _failOnCaptureError = false; 22 | var diffsCreated = []; 23 | 24 | var _resemblePath; 25 | var _resembleContainerPath; 26 | var _libraryRoot; 27 | var _rebase = false; 28 | var _prefixCount = false; 29 | var _isCount = true; 30 | 31 | var _baselineImageSuffix = ""; 32 | var _diffImageSuffix = ".diff"; 33 | var _failureImageSuffix = ".fail"; 34 | 35 | var _captureWaitEnabled = true; 36 | 37 | exports.screenshot = screenshot; 38 | exports.compareAll = compareAll; 39 | exports.compareMatched = compareMatched; 40 | exports.compareExplicit = compareExplicit; 41 | exports.compareSession = compareSession; 42 | exports.compareFiles = compareFiles; 43 | exports.waitForTests = waitForTests; 44 | exports.init = init; 45 | exports.done = done; 46 | exports.update = update; 47 | exports.turnOffAnimations = turnOffAnimations; 48 | exports.getExitStatus = getExitStatus; 49 | exports.getCreatedDiffFiles = getCreatedDiffFiles; 50 | 51 | function update( options ) { 52 | 53 | function stripslash( str ) { 54 | return ( str || '' ).replace( /\/\//g, '/' ).replace( /\\/g, '\\' ); 55 | } 56 | 57 | options = options || {}; 58 | 59 | casper = options.casper || casper; 60 | 61 | _waitTimeout = options.waitTimeout || _waitTimeout; 62 | 63 | _libraryRoot = options.libraryRoot; 64 | 65 | _resemblePath = _resemblePath || getResemblePath( _libraryRoot ); 66 | 67 | _resembleContainerPath = _resembleContainerPath || getResembleContainerPath( _libraryRoot ); 68 | 69 | _src = stripslash( options.screenshotRoot || _src ); 70 | _results = stripslash( options.comparisonResultRoot || options.screenshotRoot || _results || _src ); 71 | _failures = options.failedComparisonsRoot === false ? false : stripslash( options.failedComparisonsRoot || _failures ); 72 | 73 | _fileNameGetter = options.fileNameGetter || _fileNameGetter; 74 | 75 | _prefixCount = options.prefixCount || _prefixCount; 76 | _isCount = ( options.addIteratorToImage !== false ); 77 | 78 | _onPass = options.onPass || _onPass; 79 | _onFail = options.onFail || _onFail; 80 | _onTimeout = options.onTimeout || _onTimeout; 81 | _onNewImage = options.onNewImage || _onNewImage; 82 | _onComplete = options.onComplete || options.report || _onComplete; 83 | _onCaptureFail = options.onCaptureFail || _onCaptureFail; 84 | 85 | _failOnCaptureError = options.failOnCaptureError || _failOnCaptureError; 86 | 87 | _hideElements = options.hideElements; 88 | 89 | _mismatchTolerance = isNaN(options.mismatchTolerance) ? _mismatchTolerance : options.mismatchTolerance; 90 | 91 | _rebase = isNotUndefined(options.rebase) ? options.rebase : _rebase; 92 | 93 | _resembleOutputSettings = options.outputSettings || _resembleOutputSettings; 94 | 95 | _resembleOutputSettings.useCrossOrigin=false; // turn off x-origin attr in Resemble to support SlimerJS 96 | 97 | _cleanupComparisonImages = options.cleanupComparisonImages || _cleanupComparisonImages; 98 | 99 | _baselineImageSuffix = options.baselineImageSuffix || _baselineImageSuffix; 100 | _diffImageSuffix = options.diffImageSuffix || _diffImageSuffix; 101 | _failureImageSuffix = options.failureImageSuffix || _failureImageSuffix; 102 | 103 | _captureWaitEnabled = isNotUndefined(options.captureWaitEnabled) ? options.captureWaitEnabled : _captureWaitEnabled; 104 | 105 | if ( options.addLabelToFailedImage !== undefined ) { 106 | _addLabelToFailedImage = options.addLabelToFailedImage; 107 | } 108 | 109 | if ( _cleanupComparisonImages ) { 110 | _results += fs.separator + generateRandomString(); 111 | } 112 | } 113 | 114 | function isNotUndefined(val){ 115 | return val !== void 0; 116 | } 117 | 118 | function init( options ) { 119 | update( options ); 120 | } 121 | 122 | function done(){ 123 | _count = 0; 124 | } 125 | 126 | function getResemblePath( root ) { 127 | var path; 128 | 129 | if(root){ 130 | path = [ root, 'node_modules', 'resemblejs', 'resemble.js' ].join( fs.separator ); 131 | if ( !_isFile( path ) ) { 132 | path = [ root, '..', 'resemblejs', 'resemble.js' ].join( fs.separator ); 133 | } 134 | } else { 135 | require('resemblejs'); 136 | for(var c in require.cache) { 137 | if(/resemblejs/.test(c)) { 138 | path = require.cache[c].filename; 139 | break; 140 | } 141 | } 142 | } 143 | 144 | if ( !_isFile( path ) ) { 145 | throw "[PhantomCSS] Resemble.js not found: " + path; 146 | } 147 | 148 | return path; 149 | } 150 | 151 | 152 | function getResembleContainerPath(root) { 153 | var path; 154 | 155 | if(root){ 156 | path = root + fs.separator + 'resemblejscontainer.html'; 157 | } else { 158 | for (var c in require.cache) { 159 | if (/phantomcss/.test(c)) { 160 | path = require.cache[c].filename.replace('phantomcss.js', 'resemblejscontainer.html'); 161 | break; 162 | } 163 | } 164 | } 165 | 166 | if ( !_isFile(path) ) { 167 | throw '[PhantomCSS] Can\'t find Resemble container. (' + path + ')'; 168 | } 169 | 170 | return path; 171 | } 172 | 173 | function turnOffAnimations() { 174 | console.log( '[PhantomCSS] Turning off animations' ); 175 | casper.evaluate( function turnOffAnimations() { 176 | 177 | function disableAnimations() { 178 | var jQuery = window.jQuery; 179 | if ( jQuery ) { 180 | jQuery.fx.off = true; 181 | } 182 | 183 | var css = document.createElement( "style" ); 184 | css.type = "text/css"; 185 | css.innerHTML = "* { -webkit-transition: none !important; transition: none !important; -webkit-animation: none !important; animation: none !important; }"; 186 | document.body.appendChild( css ); 187 | } 188 | 189 | if ( document.readyState !== "loading" ) { 190 | disableAnimations(); 191 | } else { 192 | window.addEventListener( 'load', disableAnimations, false ); 193 | } 194 | } ); 195 | } 196 | 197 | function _fileNameGetter( root, fileName ) { 198 | var name; 199 | 200 | // If no iterator, enforce filename. 201 | if ( !_isCount && !fileName ) { 202 | throw 'Filename is required when addIteratorToImage option is false.'; 203 | } 204 | 205 | fileName = fileName || "screenshot"; 206 | 207 | if ( !_isCount ) { 208 | name = root + fs.separator + fileName; 209 | _count++; 210 | } else { 211 | if ( _prefixCount ) { 212 | name = root + fs.separator + _count++ + "_" + fileName; 213 | } else { 214 | name = root + fs.separator + fileName + "_" + _count++; 215 | } 216 | } 217 | 218 | if ( _isFile( name + _baselineImageSuffix + '.png' ) ) { 219 | return name + _diffImageSuffix + '.png'; 220 | } else { 221 | return name + _baselineImageSuffix + '.png'; 222 | } 223 | } 224 | 225 | function _replaceDiffSuffix( str ) { 226 | return str.replace( _diffImageSuffix, _baselineImageSuffix ); 227 | } 228 | 229 | function _isFile( path ) { 230 | var exists = false; 231 | try { 232 | exists = fs.isFile( path ); 233 | } catch ( e ) { 234 | if ( e.name !== 'NS_ERROR_FILE_TARGET_DOES_NOT_EXIST' && e.name !== 'NS_ERROR_FILE_NOT_FOUND' ) { 235 | // We weren't expecting this exception 236 | throw e; 237 | } 238 | } 239 | return exists; 240 | } 241 | 242 | function screenshot( target, timeToWait, hideSelector, fileName ) { 243 | var name; 244 | 245 | if ( isComponentsConfig( target ) ) { 246 | for ( name in target ) { 247 | if ( isComponentsConfig( target[ name ] ) ) { 248 | waitAndHideToCapture( target[ name ].selector, name, target[ name ].ignore, target[ name ].wait ); 249 | } else { 250 | waitAndHideToCapture( target[ name ], name ); 251 | } 252 | } 253 | } else { 254 | if ( isNaN( Number( timeToWait ) ) && ( typeof timeToWait === 'string' ) ) { 255 | fileName = timeToWait; 256 | timeToWait = void 0; 257 | } 258 | waitAndHideToCapture( target, fileName, hideSelector, timeToWait ); 259 | } 260 | } 261 | 262 | function isComponentsConfig( obj ) { 263 | return ( Object.prototype.toString.call( obj ) === '[object Object]' ) && ( isClipRect( obj ) === false ); 264 | } 265 | 266 | function grab( filepath, target ) { 267 | if ( isClipRect( target ) ) { 268 | casper.capture( filepath, target ); 269 | } else { 270 | casper.captureSelector( filepath, target ); 271 | } 272 | } 273 | 274 | function capture( srcPath, resultPath, target ) { 275 | var originalForResult = _replaceDiffSuffix( resultPath ); 276 | var originalFromSource = _replaceDiffSuffix( srcPath ); 277 | 278 | try { 279 | 280 | if ( _rebase ) { 281 | 282 | grab( originalFromSource, target ); 283 | 284 | if ( isThisImageADiff( resultPath ) ) { 285 | // Tidy up. Remove old diff after rebase 286 | removeFile( resultPath ); 287 | } 288 | 289 | _onNewImage( { 290 | filename: originalFromSource 291 | } ); 292 | 293 | } else if ( isThisImageADiff( resultPath ) ) { 294 | 295 | grab( resultPath, target ); 296 | 297 | diffsCreated.push( resultPath ); 298 | 299 | if ( srcPath !== resultPath ) { 300 | // also copy the original over to the result directory 301 | copyAndReplaceFile( originalFromSource, originalForResult ); 302 | } 303 | 304 | } else { 305 | 306 | grab( srcPath, target ); 307 | 308 | if ( srcPath !== resultPath ) { 309 | // can't use copyAndReplaceFile yet, so just capture again 310 | grab( resultPath, target ); 311 | } 312 | 313 | _onNewImage( { 314 | filename: resultPath 315 | } ); 316 | } 317 | 318 | } catch ( ex ) { 319 | _onCaptureFail(ex, target) 320 | } 321 | } 322 | 323 | function isClipRect( value ) { 324 | return ( 325 | typeof value === 'object' && 326 | typeof value.top === 'number' && 327 | typeof value.left === 'number' && 328 | typeof value.width === 'number' && 329 | typeof value.height === 'number' 330 | ); 331 | } 332 | 333 | function isThisImageADiff( path ) { 334 | var sanitizedDiffSuffix = _diffImageSuffix.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&" ); 335 | var diffRegex = new RegExp( sanitizedDiffSuffix + "\\.png" ); 336 | return diffRegex.test( path ); 337 | } 338 | 339 | function copyAndReplaceFile( src, dest ) { 340 | removeFile( dest ); 341 | fs.copy( src, dest ); 342 | } 343 | 344 | function removeFile( filepath ) { 345 | if ( _isFile( filepath ) ) { 346 | fs.remove( filepath ); 347 | } 348 | } 349 | 350 | function asyncCompare( one, two, func ) { 351 | 352 | if ( !casper.evaluate( function () { 353 | return window._imagediff_; 354 | } ) ) { 355 | initClient(); 356 | } 357 | 358 | casper.fillSelectors( 'form#image-diff-form', { 359 | '[name=one]': one, 360 | '[name=two]': two 361 | } ); 362 | 363 | casper.evaluate( function ( filename ) { 364 | window._imagediff_.run( filename ); 365 | }, { 366 | label: _addLabelToFailedImage ? one : false 367 | } ); 368 | 369 | casper.waitFor( 370 | function check() { 371 | return this.evaluate( function () { 372 | return window._imagediff_.hasResult; 373 | } ); 374 | }, 375 | function () { 376 | 377 | var mismatch = casper.evaluate( function () { 378 | return window._imagediff_.getResult(); 379 | } ); 380 | 381 | if ( Number( mismatch ) ) { 382 | func( false, mismatch ); 383 | } else { 384 | func( true ); 385 | } 386 | 387 | }, 388 | function () { 389 | func( false ); 390 | }, 391 | _waitTimeout 392 | ); 393 | } 394 | 395 | function getDiffs( root, collection ) { 396 | var symDict = { '..': 1, '.': 1}; 397 | if(!collection) {collection = [];} 398 | if ( fs.isDirectory( root ) ) { 399 | fs.list( root ).forEach( function(leaf){ 400 | var newroot = root + fs.separator + leaf; 401 | if ( symDict[ leaf ] ) { return true; } 402 | getDiffs(newroot, collection); 403 | } ); 404 | } else if ( isThisImageADiff( root.toLowerCase() ) ) { 405 | collection.push( root ); 406 | } 407 | return collection; 408 | } 409 | 410 | function filterOn(include, exclude){ 411 | return function(path){ 412 | var includeAble = (include === void 0) || include.test( path.toLowerCase() ); 413 | var excludeAble = exclude && exclude.test( path.toLowerCase() ); 414 | return !excludeAble && includeAble; 415 | } 416 | } 417 | 418 | function getCreatedDiffFiles() { 419 | var d = diffsCreated; 420 | diffsCreated = []; 421 | return d; 422 | } 423 | 424 | function compareMatched( match, exclude ) { 425 | // Search for diff images, but only compare matched filenames 426 | compareAll( exclude, void 0, match); 427 | } 428 | 429 | function compareExplicit( list ) { 430 | // An explicit list of diff images to compare ['/dialog.diff.png', '/header.diff.png'] 431 | compareAll( void 0, list ); 432 | } 433 | 434 | function compareSession( list ) { 435 | // compare the diffs created in this session 436 | compareAll( void 0, getCreatedDiffFiles() ); 437 | } 438 | 439 | function compareFiles( baseFile, file ) { 440 | var test = { 441 | filename: baseFile 442 | }; 443 | 444 | if ( !_isFile( baseFile ) ) { 445 | test.error = true; 446 | } else { 447 | 448 | casper.thenOpen( 'about:blank', function () {}); // reset page (fixes bug where failure screenshots leak between captures) 449 | casper.thenOpen( 'file:///' + _resembleContainerPath, function () { 450 | 451 | asyncCompare( baseFile, file, function ( isSame, mismatch ) { 452 | 453 | if ( !isSame ) { 454 | 455 | test.fail = true; 456 | 457 | casper.waitFor( 458 | function check() { 459 | return casper.evaluate( function () { 460 | return window._imagediff_.hasImage; 461 | } ); 462 | }, 463 | function () { 464 | var failFile, safeFileName, increment; 465 | 466 | if ( _failures ) { 467 | // flattened structure for failed diffs so that it is easier to preview 468 | failFile = _failures + fs.separator + file.split( /\/|\\/g ).pop().replace( _diffImageSuffix + '.png', '' ).replace( '.png', '' ); 469 | safeFileName = failFile; 470 | increment = 0; 471 | 472 | while ( _isFile( safeFileName + _failureImageSuffix + '.png' ) ) { 473 | increment++; 474 | safeFileName = failFile + '.' + increment; 475 | } 476 | 477 | failFile = safeFileName + _failureImageSuffix + '.png'; 478 | casper.captureSelector( failFile, 'img' ); 479 | 480 | test.failFile = failFile; 481 | } 482 | 483 | if ( file.indexOf( _diffImageSuffix + '.png' ) !== -1 ) { 484 | casper.captureSelector( file.replace( _diffImageSuffix + '.png', _failureImageSuffix + '.png' ), 'img' ); 485 | } else { 486 | casper.captureSelector( file.replace( '.png', _failureImageSuffix + '.png' ), 'img' ); 487 | } 488 | 489 | casper.evaluate( function () { 490 | window._imagediff_.hasImage = false; 491 | } ); 492 | 493 | if ( mismatch ) { 494 | test.mismatch = mismatch; 495 | _onFail( test ); // casper.test.fail throws and error, this function call is aborted 496 | return; // Just to make it clear what is happening 497 | } else { 498 | _onTimeout( test ); 499 | } 500 | 501 | }, 502 | function () {}, 503 | _waitTimeout 504 | ); 505 | } else { 506 | test.success = true; 507 | _onPass( test ); 508 | } 509 | 510 | } ); 511 | } ); 512 | } 513 | return test; 514 | } 515 | 516 | function str2RegExp(str){ 517 | return typeof str === 'string' ? new RegExp( str ) : str; 518 | } 519 | 520 | function compareAll( exclude, diffList, include ) { 521 | var tests = []; 522 | 523 | if ( !diffList ) { 524 | diffList = getDiffs( _results ); 525 | if(exclude || include){ 526 | diffList = diffList.filter(filterOn( str2RegExp(include), str2RegExp(exclude) )); 527 | } 528 | //diffList.forEach(function(path){console.log( '[PhantomCSS] Attempting visual comparison of ' + path );}) 529 | } 530 | 531 | diffList.forEach( function ( file ) { 532 | var baseFile = _replaceDiffSuffix( file ); 533 | tests.push( compareFiles( baseFile, file ) ); 534 | } ); 535 | 536 | waitForTests( tests ); 537 | } 538 | 539 | function waitForTests( tests ) { 540 | casper.then( function () { 541 | casper.waitFor( function () { 542 | return tests.length === tests.reduce( function ( count, test ) { 543 | if ( test.success || test.fail || test.error ) { 544 | return count + 1; 545 | } else { 546 | return count; 547 | } 548 | }, 0 ); 549 | }, function () { 550 | var fails = 0, 551 | errors = 0; 552 | tests.forEach( function ( test ) { 553 | if ( test.fail ) { 554 | fails++; 555 | } else if ( test.error ) { 556 | errors++; 557 | } 558 | } ); 559 | _onComplete( tests, fails, errors ); 560 | }, function () { 561 | 562 | }, 563 | _waitTimeout ); 564 | } ); 565 | } 566 | 567 | function initClient() { 568 | 569 | casper.page.injectJs( _resemblePath ); 570 | 571 | casper.evaluate( function ( mismatchTolerance, resembleOutputSettings ) { 572 | 573 | var result; 574 | 575 | var div = document.createElement( 'div' ); 576 | 577 | // this is a bit of hack, need to get images into browser for analysis 578 | div.style = "display:block;position:absolute;border:0;top:10px;left:0;"; 579 | // div.style = "display:block;position:absolute;border:0;top:0;left:0;height:1px;width:1px;"; 580 | div.innerHTML = '
' + 581 | '' + 582 | '' + 583 | '
'; 584 | document.body.appendChild( div ); 585 | 586 | if ( resembleOutputSettings ) { 587 | resemble.outputSettings( resembleOutputSettings ); 588 | } 589 | 590 | window._imagediff_ = { 591 | hasResult: false, 592 | hasImage: false, 593 | run: run, 594 | getResult: function () { 595 | window._imagediff_.hasResult = false; 596 | return result; 597 | } 598 | }; 599 | 600 | function run( label ) { 601 | 602 | function render( data ) { 603 | var img = new Image(); 604 | 605 | img.onload = function () { 606 | window._imagediff_.hasImage = true; 607 | }; 608 | document.getElementById( 'image-diff' ).appendChild( img ); 609 | img.src = data.getImageDataUrl( label ); 610 | } 611 | 612 | resemble( document.getElementById( 'image-diff-one' ).files[ 0 ] ). 613 | compareTo( document.getElementById( 'image-diff-two' ).files[ 0 ] ). 614 | ignoreAntialiasing(). // <-- muy importante 615 | onComplete( function ( data ) { 616 | var diffImage; 617 | var misMatchPercentage = mismatchTolerance < 0.01 ? data.rawMisMatchPercentage : data.misMatchPercentage; 618 | 619 | if ( Number( misMatchPercentage ) > mismatchTolerance ) { 620 | result = misMatchPercentage; 621 | } else { 622 | result = false; 623 | } 624 | 625 | window._imagediff_.hasResult = true; 626 | 627 | if ( Number( misMatchPercentage ) > mismatchTolerance ) { 628 | render( data ); 629 | } 630 | 631 | } ); 632 | } 633 | }, 634 | _mismatchTolerance, 635 | _resembleOutputSettings 636 | ); 637 | } 638 | 639 | function _onPass( test ) { 640 | console.log( '\n' ); 641 | var name = 'Should look the same ' + test.filename; 642 | casper.test.pass(name, {name: name}); 643 | } 644 | 645 | function _onCaptureFail( ex, target ) { 646 | console.log( "[PhantomCSS] Screenshot capture failed: " + ex.message ); 647 | if (_failOnCaptureError) { 648 | var name = 'Capture screenshot ' + target; 649 | casper.test.fail(name, {name:name, message: 'Failed to capture ' + target + ' - ' + ex.message }); 650 | } 651 | } 652 | 653 | function _onFail( test ) { 654 | console.log('\n'); 655 | var name = 'Should look the same ' + test.filename; 656 | casper.test.fail(name, {name:name, message: 'Looks different (' + test.mismatch + '% mismatch) ' + test.failFile }); 657 | } 658 | 659 | function _onTimeout( test ) { 660 | console.log( '\n' ); 661 | casper.test.info( 'Could not complete image comparison for ' + test.filename ); 662 | } 663 | 664 | function _onNewImage( test ) { 665 | console.log( '\n' ); 666 | casper.test.info( 'New screenshot at ' + test.filename ); 667 | } 668 | 669 | function _onComplete( tests, noOfFails, noOfErrors ) { 670 | 671 | if ( tests.length === 0 ) { 672 | console.log( "\nMust be your first time?" ); 673 | console.log( "Some screenshots have been generated in the directory " + _results ); 674 | console.log( "This is your 'baseline', check the images manually. If they're wrong, delete the images." ); 675 | console.log( "The next time you run these tests, new screenshots will be taken. These screenshots will be compared to the original." ); 676 | console.log( 'If they are different, PhantomCSS will report a failure.' ); 677 | } else { 678 | 679 | if ( noOfFails === 0 ) { 680 | console.log( "\nPhantomCSS found " + tests.length + " tests, None of them failed. Which is good right?" ); 681 | console.log( "\nIf you want to make them fail, change some CSS." ); 682 | } else { 683 | console.log( "\nPhantomCSS found " + tests.length + " tests, " + noOfFails + ' of them failed.' ); 684 | if ( _failures ) { 685 | console.log( '\nPhantomCSS has created some images that try to show the difference (in the directory ' + _failures + '). Fuchsia colored pixels indicate a difference betwen the new and old screenshots.' ); 686 | } 687 | } 688 | 689 | if ( noOfErrors !== 0 ) { 690 | console.log( "There were " + noOfErrors + "errors. Is it possible that a baseline image was deleted but not the diff?" ); 691 | } 692 | 693 | if ( _cleanupComparisonImages ) { 694 | fs.removeTree( _results ); 695 | } 696 | 697 | exitStatus = noOfErrors + noOfFails; 698 | } 699 | } 700 | 701 | function waitAndHideToCapture( target, fileName, hideSelector, timeToWait ) { 702 | var srcPath = _fileNameGetter( _src, fileName ); 703 | var resultPath = srcPath.replace( _src, _results ); 704 | 705 | function runCapture() { 706 | if ( hideSelector || _hideElements ) { 707 | casper.evaluate( setVisibilityToHidden, { 708 | s1: _hideElements, 709 | s2: hideSelector 710 | } ); 711 | } 712 | 713 | capture( srcPath, resultPath, target ); 714 | } 715 | if(_captureWaitEnabled) { 716 | casper.wait(timeToWait || 250, runCapture); // give a bit of time for all the images appear 717 | } else { 718 | runCapture(); 719 | } 720 | } 721 | 722 | function setVisibilityToHidden( s1, s2 ) { 723 | // executes in browser scope 724 | var selector; 725 | var elements; 726 | var i; 727 | 728 | var jQuery = window.jQuery; 729 | if ( jQuery ) { 730 | if ( s1 ) { 731 | jQuery( s1 ).css( 'visibility', 'hidden' ); 732 | } 733 | if ( s2 ) { 734 | jQuery( s2 ).css( 'visibility', 'hidden' ); 735 | } 736 | return; 737 | } 738 | 739 | // Ensure at least an empty string 740 | s1 = s1 || ''; 741 | s2 = s2 || ''; 742 | 743 | // Create a combined selector, removing leading/trailing commas 744 | selector = ( s1 + ',' + s2 ).replace( /(^,|,$)/g, '' ); 745 | elements = document.querySelectorAll( selector ); 746 | i = elements.length; 747 | 748 | while ( i-- ) { 749 | elements[ i ].style.visibility = 'hidden'; 750 | } 751 | } 752 | 753 | function getExitStatus() { 754 | return exitStatus; 755 | } 756 | 757 | function generateRandomString() { 758 | return ( Math.random() + 1 ).toString( 36 ).substring( 7 ); 759 | } 760 | -------------------------------------------------------------------------------- /readme_assets/Phantom CSS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuddleEng/PhantomCSS/100784380781bcd668b61a2f5487d0b8e874b3ca/readme_assets/Phantom CSS.png -------------------------------------------------------------------------------- /readme_assets/differentcolour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuddleEng/PhantomCSS/100784380781bcd668b61a2f5487d0b8e874b3ca/readme_assets/differentcolour.png -------------------------------------------------------------------------------- /readme_assets/false-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuddleEng/PhantomCSS/100784380781bcd668b61a2f5487d0b8e874b3ca/readme_assets/false-negative.png -------------------------------------------------------------------------------- /readme_assets/intro-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuddleEng/PhantomCSS/100784380781bcd668b61a2f5487d0b8e874b3ca/readme_assets/intro-example.png -------------------------------------------------------------------------------- /resemblejscontainer.html: -------------------------------------------------------------------------------- 1 | This blank HTML page is used for processing the images with Resemble.js --------------------------------------------------------------------------------