├── .gitignore ├── .gitmodules ├── README.md ├── chapter01 ├── recipe03.js ├── recipe04.js ├── recipe05.js ├── recipe06.js ├── recipe07-config.json ├── recipe07.js └── recipe08.js ├── chapter02 ├── reamde.txt ├── recipe01.js ├── recipe02.js ├── recipe03.js ├── recipe04.js ├── recipe05.js ├── recipe06.js ├── recipe07.js ├── recipe08.js ├── recipe09.js └── recipe11.js ├── chapter03 ├── recipe02.js ├── recipe03.js ├── recipe04.js ├── recipe05.js ├── recipe06.js ├── recipe07.js ├── recipe08.js ├── recipe09.js ├── recipe10.js ├── recipe11.js ├── recipe12.js ├── recipe13.js └── recipe14.js ├── chapter04 ├── Gruntfile.js ├── package.json ├── recipe01-runner.html ├── recipe02-runner.html ├── recipe03-runner.html ├── recipe06.conf.js ├── recipe07.conf.js ├── recipe09-runner.html └── recipe10-runner.html ├── chapter05 ├── recipe01 │ ├── pom.xml │ └── src │ │ └── test │ │ └── java │ │ └── phantomjs │ │ └── cookbook │ │ ├── PhantomJSDriverTest.java │ │ └── RemoteWebDriverTest.java ├── recipe02 │ ├── package.json │ ├── selenium-webdriver-test.js │ └── webdriverjs-test.js ├── recipe03.rb ├── recipe04.rb ├── recipe05.rb ├── recipe07.js ├── recipe08.js └── recipe10.js ├── chapter06 └── recipe06.js ├── chapter07 ├── recipe01.js ├── recipe02.js ├── recipe03.js ├── recipe04-supplement.js ├── recipe04.js ├── recipe05.js ├── recipe06.js ├── recipe07-casper.js ├── recipe07-cliprect.js └── recipe07.js ├── chapter08 ├── recipe-bonus-teamcity-runner.html ├── recipe01-runner.html ├── recipe02-runner.html └── recipe03-runner.html ├── lib ├── arg-parser.js ├── chai │ └── chai.js ├── hemingway.js ├── jasmine-reporters │ ├── jasmine.console_reporter.js │ ├── jasmine.junit_reporter.js │ ├── jasmine.nunit_reporter.js │ ├── jasmine.tap_reporter.js │ ├── jasmine.teamcity_reporter.js │ ├── jasmine.terminal_reporter.js │ ├── load_reporters.js │ ├── phantomjs-testrunner.js │ └── phantomjs.runner.sh ├── jasmine │ ├── jasmine-html.js │ ├── jasmine.css │ ├── jasmine.js │ └── json2.js ├── math-utils-spec.js ├── math-utils.js ├── mocha │ ├── mocha-1.17.1.css │ └── mocha-1.17.1.js ├── phantomcss │ ├── ResembleJs │ │ ├── LICENSE │ │ ├── README.md │ │ ├── resemble.js │ │ └── resemblejscontainer.html │ └── phantomcss.js ├── qunit │ ├── qunit-1.14.0.css │ └── qunit-1.14.0.js ├── string-utils-expectations.js ├── string-utils-spec.js ├── string-utils-tests.js ├── string-utils.js └── yslow.js └── phantomjs-sandbox ├── .bowerrc ├── app.js ├── bower.json ├── package.json ├── routes └── index.js ├── static ├── components │ ├── bootstrap │ │ ├── .bower.json │ │ ├── DOCS-LICENSE │ │ ├── LICENSE │ │ ├── LICENSE-MIT │ │ ├── README.md │ │ ├── bower.json │ │ ├── dist │ │ │ ├── css │ │ │ │ ├── bootstrap-theme.css │ │ │ │ ├── bootstrap-theme.min.css │ │ │ │ ├── bootstrap.css │ │ │ │ └── bootstrap.min.css │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ └── glyphicons-halflings-regular.woff │ │ │ └── js │ │ │ │ ├── bootstrap.js │ │ │ │ └── bootstrap.min.js │ │ ├── js │ │ │ ├── affix.js │ │ │ ├── alert.js │ │ │ ├── button.js │ │ │ ├── carousel.js │ │ │ ├── collapse.js │ │ │ ├── dropdown.js │ │ │ ├── modal.js │ │ │ ├── popover.js │ │ │ ├── scrollspy.js │ │ │ ├── tab.js │ │ │ ├── tooltip.js │ │ │ └── transition.js │ │ └── less │ │ │ ├── alerts.less │ │ │ ├── badges.less │ │ │ ├── bootstrap.less │ │ │ ├── breadcrumbs.less │ │ │ ├── button-groups.less │ │ │ ├── buttons.less │ │ │ ├── carousel.less │ │ │ ├── close.less │ │ │ ├── code.less │ │ │ ├── component-animations.less │ │ │ ├── dropdowns.less │ │ │ ├── forms.less │ │ │ ├── glyphicons.less │ │ │ ├── grid.less │ │ │ ├── input-groups.less │ │ │ ├── jumbotron.less │ │ │ ├── labels.less │ │ │ ├── list-group.less │ │ │ ├── media.less │ │ │ ├── mixins.less │ │ │ ├── modals.less │ │ │ ├── navbar.less │ │ │ ├── navs.less │ │ │ ├── normalize.less │ │ │ ├── pager.less │ │ │ ├── pagination.less │ │ │ ├── panels.less │ │ │ ├── popovers.less │ │ │ ├── print.less │ │ │ ├── progress-bars.less │ │ │ ├── responsive-utilities.less │ │ │ ├── scaffolding.less │ │ │ ├── tables.less │ │ │ ├── theme.less │ │ │ ├── thumbnails.less │ │ │ ├── tooltip.less │ │ │ ├── type.less │ │ │ ├── utilities.less │ │ │ ├── variables.less │ │ │ └── wells.less │ ├── jquery │ │ ├── .bower.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── bower.json │ │ ├── component.json │ │ ├── composer.json │ │ ├── jquery-migrate.js │ │ ├── jquery-migrate.min.js │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ ├── jquery.min.map │ │ └── package.json │ └── moment │ │ ├── .bower.json │ │ ├── LICENSE │ │ ├── bower.json │ │ ├── lang │ │ ├── ar-ma.js │ │ ├── ar.js │ │ ├── bg.js │ │ ├── br.js │ │ ├── bs.js │ │ ├── ca.js │ │ ├── cs.js │ │ ├── cv.js │ │ ├── cy.js │ │ ├── da.js │ │ ├── de.js │ │ ├── el.js │ │ ├── en-au.js │ │ ├── en-ca.js │ │ ├── en-gb.js │ │ ├── eo.js │ │ ├── es.js │ │ ├── et.js │ │ ├── eu.js │ │ ├── fa.js │ │ ├── fi.js │ │ ├── fo.js │ │ ├── fr-ca.js │ │ ├── fr.js │ │ ├── gl.js │ │ ├── he.js │ │ ├── hi.js │ │ ├── hr.js │ │ ├── hu.js │ │ ├── hy-am.js │ │ ├── id.js │ │ ├── is.js │ │ ├── it.js │ │ ├── ja.js │ │ ├── ka.js │ │ ├── ko.js │ │ ├── lb.js │ │ ├── lt.js │ │ ├── lv.js │ │ ├── mk.js │ │ ├── ml.js │ │ ├── mr.js │ │ ├── ms-my.js │ │ ├── nb.js │ │ ├── ne.js │ │ ├── nl.js │ │ ├── nn.js │ │ ├── pl.js │ │ ├── pt-br.js │ │ ├── pt.js │ │ ├── ro.js │ │ ├── rs.js │ │ ├── ru.js │ │ ├── sk.js │ │ ├── sl.js │ │ ├── sq.js │ │ ├── sv.js │ │ ├── ta.js │ │ ├── th.js │ │ ├── tl-ph.js │ │ ├── tr.js │ │ ├── tzm-la.js │ │ ├── tzm.js │ │ ├── uk.js │ │ ├── uz.js │ │ ├── vn.js │ │ ├── zh-cn.js │ │ └── zh-tw.js │ │ ├── min │ │ ├── langs.js │ │ ├── langs.min.js │ │ ├── moment-with-langs.js │ │ ├── moment-with-langs.min.js │ │ └── moment.min.js │ │ ├── moment.js │ │ └── readme.md ├── css │ ├── form-demo.css │ └── index.css ├── images │ ├── 152824439_ffcc1b2aa4_b.jpg │ ├── 357292530_f225d7e306_b.jpg │ ├── 391560246_f2ac936f6d_b.jpg │ ├── 583519989_1116956980_b.jpg │ ├── 872027465_2519a358b9_b.jpg │ ├── form-demo-bg.jpg │ └── phantomjs-logo.png ├── js │ ├── appcache-demo.js │ ├── form-demo-validators.js │ ├── form-demo.js │ └── svg-demo.js └── svg │ └── eyes.svg ├── tests ├── e2e │ └── chapter08-recipe05-spec.js ├── test-wrapper.sh └── unit │ ├── form-demo-validators-runner.html │ └── form-demo-validators-spec.js ├── views-list.js └── views ├── appcache-demo.ejs ├── cache-demo.ejs ├── cdn-demo.ejs ├── cookie-demo.ejs ├── css-demo.ejs ├── form-demo.ejs ├── hover-demo.ejs ├── index.ejs ├── input-demo.ejs ├── precision-click.ejs ├── responsive-demo.ejs └── svg-demo.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | cookie-jar.txt 2 | foo-log/ 3 | twitter-*.png 4 | 5 | demo-with*-css.png 6 | 7 | chapter04/node_modules/ 8 | chapter04/coverage/ 9 | 10 | chapter05/recipe02/node_modules/ 11 | chapter05*.png 12 | 13 | recipe09.xml 14 | screenshots/ 15 | failures/ 16 | 17 | lib/confess/ 18 | phantomjs-sandbox/static/demo.appcache 19 | 20 | phantomjs-sandbox/static/components/snap.svg/ 21 | phantomjs-sandbox/node_modules/ 22 | 23 | test-reports/ 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/yslow"] 2 | path = lib/yslow 3 | url = https://github.com/marcelduran/yslow.git 4 | -------------------------------------------------------------------------------- /chapter01/recipe03.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 3 | Running a PhantomJS script 6 | */ 7 | console.log('A console statement from PhantomJS on ' + new Date().toDateString() + '!'); 8 | 9 | phantom.exit(); 10 | -------------------------------------------------------------------------------- /chapter01/recipe04.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 4 | Running a PhantomJS script with arguments 6 | */ 7 | if (phantom.args.length === 0) { 8 | console.log('No arguments were passed in.'); 9 | } else { 10 | phantom.args.forEach(function(arg, index) { 11 | console.log('[' + index + '] ' + arg); 12 | }); 13 | } 14 | 15 | phantom.exit(); -------------------------------------------------------------------------------- /chapter01/recipe05.js: -------------------------------------------------------------------------------- 1 | /*jshint phantom:true, devel:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 5 | Running PhantomJS with cookies 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.open('http://localhost:3000/cookie-demo', function(status) { 10 | if (status === 'success') { 11 | phantom.cookies.forEach(function(cookie, i) { 12 | for (var key in cookie) { 13 | console.log('[cookie:' + i + '] ' + key + ' = ' + cookie[key]); 14 | } 15 | }); 16 | 17 | phantom.exit(); 18 | } else { 19 | console.error('Could not open the page! (Is it running?)'); 20 | phantom.exit(1); 21 | } 22 | }); -------------------------------------------------------------------------------- /chapter01/recipe06.js: -------------------------------------------------------------------------------- 1 | /*jshint phantom:true, devel:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 6 | Running PhantomJS with a disk cache 6 | */ 7 | var page = require('webpage').create(), 8 | count = 0, 9 | until = 2; 10 | 11 | page.onResourceReceived = function(res) { 12 | if (res.stage === 'end') { 13 | console.log(JSON.stringify(res, undefined, 2)); 14 | } 15 | }; 16 | 17 | page.onLoadStarted = function() { 18 | count += 1; 19 | console.log('Run ' + count + ' of ' + until + '.'); 20 | }; 21 | 22 | page.onLoadFinished = function(status) { 23 | if (status === 'success') { 24 | if (count < until) { 25 | console.log('Go again.\n'); 26 | page.reload(); 27 | } else { 28 | console.log('All done.'); 29 | phantom.exit(); 30 | } 31 | } else { 32 | console.error('Could not open page! (Is it running?)'); 33 | phantom.exit(1); 34 | } 35 | }; 36 | 37 | page.open('http://localhost:3000/cache-demo'); -------------------------------------------------------------------------------- /chapter01/recipe07-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookiesFile" : "cookie-jar.txt", 3 | "ignoreSslErrors" : true 4 | } -------------------------------------------------------------------------------- /chapter01/recipe07.js: -------------------------------------------------------------------------------- 1 | /*jshint phantom:true, devel:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 7 | Running PhantomJS with a JSON configuration file 6 | */ 7 | require('webpage') 8 | .create() 9 | .open('http://localhost:3000/cookie-demo', function(status) { 10 | if (status === 'success') { 11 | phantom.cookies.forEach(function(cookie, i) { 12 | for (var key in cookie) { 13 | console.log('[cookie:' + i + '] ' + key + ' = ' + cookie[key]); 14 | } 15 | }); 16 | 17 | phantom.exit(); 18 | } else { 19 | console.error('Could not open the page! (Is it running?)'); 20 | phantom.exit(1); 21 | } 22 | }); -------------------------------------------------------------------------------- /chapter01/recipe08.js: -------------------------------------------------------------------------------- 1 | /*jshint phantom:true, devel:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 1 | Getting Started with PhantomJS 5 | * Recipe 8 | Debugging a PhantomJS script 6 | */ 7 | var page = require('webpage').create(); 8 | 9 | page.onResourceReceived = function(res) { 10 | if (res.stage === 'end') { 11 | console.log(JSON.stringify(res, undefined, 2)); 12 | } 13 | }; 14 | 15 | page.open('http://localhost:3000/cache-demo', function(status) { 16 | if (status === 'success') { 17 | console.log('All done.'); 18 | phantom.exit(); 19 | } else { 20 | console.error('Could not open page! (Is it running?)'); 21 | phantom.exit(1); 22 | } 23 | }); -------------------------------------------------------------------------------- /chapter02/reamde.txt: -------------------------------------------------------------------------------- 1 | The Big U 2 | Zodiac 3 | Snow Crash 4 | The Diamond Age: or A Young Lady's Illustrated Primer 5 | Cryptonomicon 6 | Quicksilver 7 | The Confusion 8 | The System of the World 9 | Anathem 10 | Reamde 11 | -------------------------------------------------------------------------------- /chapter02/recipe01.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 1 | Inspecting the version at runtime 6 | */ 7 | console.log('PhantomJS'); 8 | console.log(' - major version: ' + phantom.version.major); 9 | console.log(' - minor version: ' + phantom.version.minor); 10 | console.log(' - patch version: ' + phantom.version.patch); 11 | phantom.exit(); -------------------------------------------------------------------------------- /chapter02/recipe02.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 2 | Managing cookies with the phantom object 6 | */ 7 | var page = require('webpage').create(); 8 | 9 | if (!phantom.cookiesEnabled) { 10 | console.log('Note: cookies not enabled.'); 11 | } 12 | 13 | page.open('http://localhost:3000/cookie-demo', function(status) { 14 | if (status === 'success') { 15 | console.log('We start with these cookies:'); 16 | phantom.cookies.forEach(function(c) { 17 | console.info(JSON.stringify(c, undefined, 2)); 18 | }); 19 | 20 | phantom.addCookie({ 21 | name: 'jerry', 22 | value: 'black-and-white', 23 | domain: 'localhost' 24 | }); 25 | 26 | console.log('Added the "jerry" cookie; how many now? ' + phantom.cookies.length); 27 | 28 | phantom.deleteCookie('jerry'); 29 | console.log('Deleted the "jerry" cookie; how many now? ' + phantom.cookies.length); 30 | 31 | phantom.clearCookies(); 32 | 33 | console.log('How many cookies after a clear? ' + phantom.cookies.length); 34 | 35 | phantom.exit(); 36 | } else { 37 | console.error('Something is wrong!'); 38 | phantom.exit(1); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /chapter02/recipe03.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /*global fibonacci */ 3 | /** 4 | * PhantomJS Cookbook 5 | * Chapter 2 | PhantomJS Core Modules 6 | * Recipe 3 | Specifying a path for external scripts 7 | */ 8 | var foo = 'foo'; 9 | 10 | console.log('Initial libraryPath: ' + phantom.libraryPath); 11 | 12 | phantom.libraryPath = phantom.libraryPath.replace(/chapter02$/, 'lib'); 13 | 14 | console.log('Updated libraryPath: ' + phantom.libraryPath); 15 | 16 | var isInjected = phantom.injectJs('hemingway.js'); 17 | 18 | if (isInjected) { 19 | console.log('Script was successfully injected.'); 20 | console.log('Give me some Fibonacci numbers! ' + 21 | fibonacci(Math.round(Math.random() * 10) + 1)); 22 | 23 | phantom.exit(); 24 | } else { 25 | console.log('Failed to inject script.'); 26 | phantom.exit(1); 27 | } -------------------------------------------------------------------------------- /chapter02/recipe04.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 4 | Setting up a global PhantomJS error handler 6 | */ 7 | phantom.onError = function(message, trace) { 8 | console.error('[PHANTOMJS ERROR] ' + message); 9 | trace.forEach(function(t) { 10 | console.error(' >> [' + t.line + '] ' + 11 | (t.function ? '[' + t.function + '] ' : '') + 12 | t.file || t.sourceURL); 13 | }); 14 | phantom.exit(1); 15 | }; 16 | 17 | function doSomeErrorProneStuff() { 18 | throw new Error('Gremlins fed after midnight.'); 19 | } 20 | 21 | doSomeErrorProneStuff(); 22 | 23 | console.log('Exiting cleanly.'); 24 | phantom.exit(0); 25 | -------------------------------------------------------------------------------- /chapter02/recipe05.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 5 | Controlling the exit status of a PhantomJS script 6 | */ 7 | console.log('Running the PhantomJS exit demo...'); 8 | 9 | if (Math.floor(Math.random() * 10) % 2 === 0) { 10 | console.log('Exiting cleanly from PhantomJS!'); 11 | phantom.exit(); 12 | } else { 13 | console.log('Exiting with an error status.'); 14 | phantom.exit(1); 15 | } -------------------------------------------------------------------------------- /chapter02/recipe06.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 6 | Inspecting command-line arguments 6 | */ 7 | var system = require('system'), 8 | args = system.args; 9 | 10 | console.log('script name is: ' + args[0]); 11 | 12 | if (args.length > 1) { 13 | var restArgs = args.slice(1); 14 | restArgs.forEach(function(arg, i) { 15 | console.log('[' + (i + 1) + '] ' + arg); 16 | }); 17 | } else { 18 | console.log('No arguments were passed.'); 19 | } 20 | 21 | phantom.exit(); -------------------------------------------------------------------------------- /chapter02/recipe07.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 7 | Inspecting system environment variables 6 | */ 7 | var env = require('system').env, 8 | prop = 'BOOK_TITLE'; 9 | 10 | var keys = Object.keys(env).filter(function(k) { 11 | return k === prop; 12 | }); 13 | 14 | if (keys.length === 1) { 15 | console.log(keys[0] + ' = ' + env[keys[0]]); 16 | } else { 17 | console.log('Could not find a property in env called ' + prop); 18 | } 19 | 20 | phantom.exit(); -------------------------------------------------------------------------------- /chapter02/recipe08.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 8 | Saving a file from a PhantomJS script 6 | */ 7 | var fs = require('fs'), 8 | targetDir = 'foo-log'; 9 | 10 | if (!fs.exists(targetDir)) { 11 | console.log('Creating directory ' + targetDir); 12 | fs.makeDirectory(targetDir); 13 | } 14 | 15 | if (!fs.isWritable(targetDir)) { 16 | console.error(targetDir + ' is not writable!'); 17 | phantom.exit(1); 18 | } 19 | 20 | console.log('Writing file...'); 21 | var currentTime = new Date().getTime(); 22 | fs.write(targetDir + fs.separator + currentTime + '.txt', 23 | 'Current time is ' + currentTime, 'w'); 24 | 25 | phantom.exit(); -------------------------------------------------------------------------------- /chapter02/recipe09.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 9 | Reading a file from PhantomJS 6 | */ 7 | phantom.onError = function(message, trace) { 8 | console.error('[Something went wrong!] - ' + message); 9 | phantom.exit(1); 10 | }; 11 | 12 | var fs = require('fs'), 13 | _name = 'reamde.txt', 14 | path = require('system').args[0].split(fs.separator); 15 | 16 | path = path.slice(0, path.length - 1).join(fs.separator); 17 | 18 | fs.changeWorkingDirectory(path); 19 | 20 | var file = fs.open(_name, 'r'); 21 | 22 | console.log('[Reading ' + _name + '...]'); 23 | while (!file.atEnd()) { 24 | console.log(file.readLine()); 25 | } 26 | 27 | console.log('[Closing ' + _name + '.]'); 28 | file.close(); 29 | 30 | phantom.exit(); -------------------------------------------------------------------------------- /chapter02/recipe11.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 2 | PhantomJS Core Modules 5 | * Recipe 11 | Loading a custom module in PhantomJS 6 | */ 7 | var argParser = require('../lib/arg-parser'), 8 | args = require('system').args.slice(1); 9 | 10 | args = argParser.parseArgs(args); 11 | 12 | Object.keys(args).forEach(function(k) { 13 | console.log(k + ' = ' + args[k] + ' (' + (typeof args[k]) + ')'); 14 | }); 15 | 16 | phantom.exit(); -------------------------------------------------------------------------------- /chapter03/recipe02.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 2 | Opening a URL within PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.open('http://blog.founddrama.net/', function(status) { 10 | switch (status) { 11 | case 'success': 12 | console.log('webpage opened successfully'); 13 | phantom.exit(0); 14 | break; 15 | case 'fail': 16 | console.error('webpage did not open successfully'); 17 | phantom.exit(1); 18 | break; 19 | default: 20 | console.error('webpage opened with unknown status: ' + status); 21 | phantom.exit(1); 22 | } 23 | }); -------------------------------------------------------------------------------- /chapter03/recipe03.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 3 | Generating a POST from PhantomJS 6 | */ 7 | var webpage = require('webpage').create(), 8 | url = 'http://localhost:3000/post-demo', 9 | postData = JSON.stringify({ 10 | "foo": "bar", 11 | "now": new Date().getTime() 12 | }); 13 | 14 | webpage.customHeaders = { "Content-Type":"application/json" }; 15 | 16 | webpage.onInitialized = function() { 17 | webpage.customHeaders = {}; 18 | }; 19 | 20 | webpage.open(url, 'POST', postData, function(status) { 21 | if (status === 'fail') { 22 | console.error('Something went wrong posting to ' + url); 23 | phantom.exit(1); 24 | } 25 | 26 | console.log('Successful post to ' + url); 27 | phantom.exit(0); 28 | }); 29 | -------------------------------------------------------------------------------- /chapter03/recipe04.js: -------------------------------------------------------------------------------- 1 | /*jshint curly:false, devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 4 | Inspecting page content from a PhantomJS script 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.open('http://blog.founddrama.net/', function(status) { 10 | if (status === 'fail') { 11 | console.error('Failed to open requested page.'); 12 | phantom.exit(1); 13 | } 14 | 15 | var titles = webpage.evaluate(function(selector) { 16 | var titles = [], 17 | forEach = Array.prototype.forEach, 18 | nodes = document.querySelectorAll(selector); 19 | 20 | forEach.call(nodes, function(el) { 21 | titles.push(el.innerText); 22 | }); 23 | 24 | return titles; 25 | }, '.post h2'); 26 | 27 | titles.forEach(function(t) { 28 | console.log(t); 29 | }); 30 | 31 | phantom.exit(); 32 | }); 33 | -------------------------------------------------------------------------------- /chapter03/recipe05.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 5 | Including external JavaScript on the page 6 | */ 7 | var webpage = require('webpage').create(), 8 | script = '../lib/hemingway.js', 9 | jquery = 'http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js'; 10 | 11 | webpage.open('http://localhost:3000/', function(status) { 12 | if (status === 'fail') { 13 | console.error('Failed to open web page.'); 14 | phantom.exit(1); 15 | } 16 | 17 | if (webpage.injectJs(script)) { 18 | webpage.includeJs(jquery, function() { 19 | var fibs = webpage.evaluate(function() { 20 | var $ct = $('
').appendTo('body'), 21 | seed = Math.ceil(Math.random() * 10), 22 | fibs = []; 23 | 24 | fibonacci(seed).forEach(function(n) { 25 | $ct.append('
' + n + '
'); 26 | }); 27 | 28 | 29 | $('.fib').each(function(i, el) { 30 | fibs.push(el.innerText); 31 | }); 32 | 33 | return fibs; 34 | }); 35 | 36 | console.log('Fibonacci numbers inserted included:'); 37 | fibs.forEach(function(n) { 38 | console.log(' \u20D7 ' + n); 39 | }); 40 | 41 | phantom.exit(); 42 | }); 43 | } else { 44 | console.error('Something went wrong trying to inject ' + script); 45 | phantom.exit(1); 46 | } 47 | }); -------------------------------------------------------------------------------- /chapter03/recipe06.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 6 | Recording debugger messages 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.onConsoleMessage = function(message, lineNum, sourceId) { 10 | console.log('[phantomjs:page] ' + message); 11 | }; 12 | 13 | webpage.evaluate(function(url) { 14 | console.log('Hello from inside of ' + url); 15 | }, webpage.url); 16 | 17 | phantom.exit(); 18 | -------------------------------------------------------------------------------- /chapter03/recipe07.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 7 | Simulating mouse clicks in PhantomJS 6 | */ 7 | var webpage = require('webpage').create(), 8 | url = 'http://localhost:3000/'; 9 | 10 | webpage.viewportSize = { width: 1280, height: 800 }; 11 | 12 | webpage.onUrlChanged = function(targetUrl) { 13 | console.log('Latest URL: ' + targetUrl); 14 | }; 15 | 16 | webpage.onLoadFinished = function(status) { 17 | if (status === 'fail') { 18 | console.error('webpage did not open successfully'); 19 | phantom.exit(1); 20 | } 21 | 22 | if (webpage.url !== url) { 23 | console.log('URL changed; exiting...'); 24 | phantom.exit(); 25 | } 26 | 27 | var coords = webpage.evaluate(function() { 28 | var firstLink = document.querySelector('a'); 29 | 30 | return { 31 | x: firstLink.offsetLeft, 32 | y: firstLink.offsetTop 33 | }; 34 | }); 35 | 36 | webpage.sendEvent('click', coords.x, coords.y); 37 | }; 38 | 39 | webpage.open(url); -------------------------------------------------------------------------------- /chapter03/recipe08.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 8 | Simulating keyboard input in PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.viewportSize = { width: 1280, height: 800 }; 10 | 11 | function getStageValue() { 12 | return webpage.evaluate(function() { 13 | return document.querySelector('#stage').innerText || ''; 14 | }); 15 | } 16 | 17 | webpage.open('http://localhost:3000/input-demo', function(status) { 18 | if (status === 'fail') { 19 | console.error('webpage did not open successfully'); 20 | phantom.exit(1); 21 | } 22 | 23 | console.log('Starting #stage text is: ' + getStageValue()); 24 | 25 | webpage.evaluate(function() { 26 | document.querySelector('#demo').focus(); 27 | }); 28 | 29 | webpage.sendEvent('keypress', 'phantomjs'); 30 | webpage.sendEvent('keypress', webpage.event.key.Enter); 31 | 32 | console.log('After input, #stage value is: ' + getStageValue()); 33 | 34 | phantom.exit(); 35 | }); -------------------------------------------------------------------------------- /chapter03/recipe09.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 9 | Simulating scrolling in PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.viewportSize = { width: 1280, height: 800 }; 10 | webpage.scrollPosition = { top: 0, left: 0 }; 11 | 12 | webpage.open('https://twitter.com/founddrama', function(status) { 13 | if (status === 'fail') { 14 | console.error('webpage did not open successfully'); 15 | phantom.exit(1); 16 | } 17 | 18 | var i = 0, 19 | top, 20 | queryFn = function() { 21 | return document.body.scrollHeight; 22 | }; 23 | 24 | setInterval(function() { 25 | var filename = 'twitter-' + (++i) + '.png'; 26 | console.log('Writing ' + filename + '...'); 27 | webpage.render(filename); 28 | 29 | top = webpage.evaluate(queryFn); 30 | 31 | console.log('[' + i + '] top = ' + top); 32 | webpage.scrollPosition = { top: top + 1, left: 0 }; 33 | 34 | if (i >= 5) { 35 | phantom.exit(); 36 | } 37 | }, 3000); 38 | }); -------------------------------------------------------------------------------- /chapter03/recipe10.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 10 | Simulating mouse hovers in PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.viewportSize = { width: 1280, height: 800 }; 10 | 11 | webpage.onConsoleMessage = function(m) { 12 | console.log(m); 13 | phantom.exit(); 14 | }; 15 | 16 | webpage.open('http://localhost:3000/hover-demo', function(status) { 17 | if (status === 'fail') { 18 | console.error('webpage did not open successfully'); 19 | phantom.exit(1); 20 | } 21 | 22 | var coords = webpage.evaluate(function() { 23 | var box = document.querySelector('.hover-demo'); 24 | 25 | return { x: box.offsetLeft, y: box.offsetTop }; 26 | }); 27 | 28 | webpage.sendEvent('mousemove', coords.x + 10, coords.y + 10); 29 | }); -------------------------------------------------------------------------------- /chapter03/recipe11.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 11 | Blocking CSS from downloading 6 | */ 7 | var webpage = require('webpage').create(), 8 | url = 'http://localhost:3000/css-demo', 9 | cssRx = /\.css\??.*$/i, 10 | count = 0; 11 | 12 | webpage.viewportSize = { width: 1024, height: 768 }; 13 | 14 | webpage.clipRect = { 15 | top: 0, 16 | left: 0, 17 | width: 1280, 18 | height: 800 19 | }; 20 | 21 | webpage.onLoadStarted = function() { 22 | count += 1; 23 | }; 24 | 25 | webpage.onResourceRequested = function(requestData, networkRequest) { 26 | if (count > 1 && cssRx.test(requestData.url)) { 27 | console.log('Dropping CSS for ' + url); 28 | networkRequest.abort(); 29 | } 30 | }; 31 | 32 | webpage.onLoadFinished = function(status) { 33 | if (status === 'fail') { 34 | console.error(url + ' did not open successfully'); 35 | phantom.exit(1); 36 | } 37 | 38 | if (count <= 1) { 39 | console.log('Rendering ' + url + ' with CSS...'); 40 | webpage.render('demo-with-css.png'); 41 | webpage.reload(); 42 | } else { 43 | console.log('Rendering ' + url + ' without CSS...'); 44 | webpage.render('demo-without-css.png'); 45 | phantom.exit(); 46 | } 47 | }; 48 | 49 | webpage.open(url); -------------------------------------------------------------------------------- /chapter03/recipe12.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 12 | Causing images to fail randomly 6 | */ 7 | var webpage = require('webpage').create(), 8 | url = 'http://localhost:3000/cache-demo', 9 | imgRx = /\.(?:gif|png|jpe?g)$/i, 10 | requestsMade = 0, 11 | requestsCanceled = 0; 12 | 13 | webpage.viewportSize = { width: 1280, height: 800 }; 14 | 15 | webpage.onResourceRequested = function(requestData, networkRequest) { 16 | if (imgRx.test(requestData.url)) { 17 | requestsMade += 1; 18 | if (Math.floor(Math.random() * 10) % 3 === 0) { 19 | requestsCanceled += 1; 20 | networkRequest.abort(); 21 | } 22 | } 23 | }; 24 | 25 | webpage.onResourceError = function(resourceError) { 26 | console.error('Error with requested resource:\n' + JSON.stringify(resourceError, undefined, 2)); 27 | }; 28 | 29 | console.log('Simulating poor network weather for ' + url); 30 | webpage.open(url, function(status) { 31 | if (status === 'fail') { 32 | console.error(url + ' did not open successfully.'); 33 | phantom.exit(1); 34 | } 35 | 36 | console.log('Canceled ' + requestsCanceled + ' of ' + requestsMade + ' image requests.'); 37 | phantom.exit(); 38 | }); -------------------------------------------------------------------------------- /chapter03/recipe13.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 13 | Submitting Ajax requests from PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.onResourceReceived = function(response) { 10 | if (response.stage === 'end') { 11 | console.log('Content-Type: ' + response.contentType); 12 | } 13 | }; 14 | 15 | webpage.open('http://localhost:3000/ajax-demo', function(status) { 16 | if (status === 'fail') { 17 | console.error('webpage did not open successfully'); 18 | phantom.exit(1); 19 | } 20 | 21 | console.log(webpage.plainText); 22 | phantom.exit(); 23 | }); -------------------------------------------------------------------------------- /chapter03/recipe14.js: -------------------------------------------------------------------------------- 1 | /*jshint devel:true, phantom:true*/ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 3 | Working with webpage Objects 5 | * Recipe 14 | Working with WebSockets in PhantomJS 6 | */ 7 | var webpage = require('webpage').create(); 8 | 9 | webpage.onConsoleMessage = function(m) { 10 | console.log(m); 11 | 12 | if (/^Closing WebSocket/.test(m)) { 13 | phantom.exit(); 14 | } 15 | }; 16 | 17 | webpage.open('http://localhost:3000/', function(status) { 18 | if (status === 'fail') { 19 | console.error('webpage did not open successfully'); 20 | phantom.exit(1); 21 | } 22 | 23 | webpage.evaluateAsync(function() { 24 | var ws = new WebSocket('ws://localhost:3000/'); 25 | 26 | function stringify(o) { 27 | return JSON.stringify(o, undefined, 2); 28 | } 29 | 30 | ws.onopen = function(event) { 31 | console.log('WebSocket opened...\n' + stringify(event)); 32 | 33 | ws.send('ping'); 34 | }; 35 | ws.onmessage = function(event) { 36 | console.log('WebSocket message:\n' + stringify(event)); 37 | }; 38 | ws.onerror = function(event) { 39 | console.error('WebSocket error!\n' + stringify(event)); 40 | }; 41 | ws.onclose = function(event) { 42 | console.error('Closing WebSocket...\n' + stringify(event)); 43 | }; 44 | 45 | console.log('WebSocket created...\n' + stringify(ws)); 46 | 47 | setTimeout(function() { 48 | ws.close(); 49 | }, 1000); 50 | }); 51 | }); -------------------------------------------------------------------------------- /chapter04/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | /** 5 | * PhantomJS Cookbook 6 | * Chapter 4 | Unit Testing with PhantomJS 7 | * Recipe 4 | Running Jasmine unit tests with Grunt 8 | */ 9 | jasmine: { 10 | recipe04: { 11 | src: '../lib/string-utils.js', 12 | options: { 13 | specs: '../lib/string*-spec.js' 14 | } 15 | } 16 | }, 17 | /** 18 | * PhantomJS Cookbook 19 | * Chapter 4 | Unit Testing with PhantomJS 20 | * Recipe 5 | Watching your tests during development with Grunt 21 | */ 22 | watch: { 23 | scripts: { 24 | files: ['../lib/*.js'], 25 | tasks: ['jasmine'] 26 | } 27 | } 28 | }); 29 | 30 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 31 | grunt.loadNpmTasks('grunt-contrib-watch'); 32 | grunt.loadNpmTasks('grunt-notify'); 33 | 34 | grunt.registerTask('test', ['jasmine']); 35 | }; -------------------------------------------------------------------------------- /chapter04/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomjs-cookbook-chapter04", 3 | "description": "The sample code to go along with Chapter 4 of The PhantomJS Cookbook.", 4 | "version": "0.0.1", 5 | "devDependencies": { 6 | "grunt": "~0.4.2", 7 | "grunt-contrib-jasmine": "~0.5.2", 8 | "grunt-contrib-watch": "~0.5.3", 9 | "grunt-notify": "~0.2.17", 10 | "karma-script-launcher": "~0.1.0", 11 | "karma-chrome-launcher": "~0.1.2", 12 | "karma-firefox-launcher": "~0.1.3", 13 | "karma-html2js-preprocessor": "~0.1.0", 14 | "karma-coffee-preprocessor": "~0.1.2", 15 | "requirejs": "~2.1.10", 16 | "karma-requirejs": "~0.2.1", 17 | "karma-phantomjs-launcher": "~0.1.1", 18 | "karma": "~0.10.9", 19 | "karma-jasmine": "~0.1.5", 20 | "karma-coverage": "~0.1.5", 21 | "mocha-phantomjs": "~3.3.1" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /chapter04/recipe01-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /chapter04/recipe02-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /chapter04/recipe03-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 36 | 37 | -------------------------------------------------------------------------------- /chapter04/recipe06.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 4 | Unit Testing with PhantomJS 4 | * Recipe 6 | Running Jasmine unit tests with the Karma test runner 5 | */ 6 | module.exports = function(config) { 7 | config.set({ 8 | frameworks: ['jasmine'], 9 | files: [ 10 | '../lib/string-utils.js', 11 | '../lib/string-utils-spec.js' 12 | ], 13 | browsers: ['PhantomJS'], 14 | singleRun: true 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /chapter04/recipe07.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 4 | Unit Testing with PhantomJS 4 | * Recipe 7 | Generating code coverage reports with Istanbul and the Karma test runner 5 | */ 6 | module.exports = function(config) { 7 | config.set({ 8 | frameworks: ['jasmine'], 9 | files: [ 10 | '../lib/string-utils.js', 11 | '../lib/string-utils-spec.js' 12 | ], 13 | preprocessors: { 14 | '../lib/string-utils.js': 'coverage' 15 | }, 16 | reporters: ['progress', 'coverage'], 17 | coverageReporter: { 18 | type: 'html', 19 | dir: 'coverage/' 20 | }, 21 | browsers: ['PhantomJS'], 22 | singleRun: true 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /chapter04/recipe09-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #9 5 | 6 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chapter04/recipe10-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The PhantomJS Cookbook - Chapter Four - Recipe #10 6 | 7 | 12 | 13 | 14 |
15 | 16 | 17 | 22 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /chapter05/recipe01/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | phantomjs.cookbook 6 | chapter05-recipe01 7 | jar 8 | 1.0-SNAPSHOT 9 | chapter05-recipe01 10 | 11 | Recipe #1 (Running Selenium Tests with PhantomJS and GhostDriver) of 12 | Chapter 5 (Functional and End-to-End Testing with PhantomJS) of 13 | The PhantomJS Cookbook -- wherein we demonstrate how to fit PhantomJS into 14 | a Selenium-based functional testing workflow. 15 | 16 | http://maven.apache.org 17 | 18 | 19 | org.seleniumhq.selenium 20 | selenium-java 21 | 2.39.0 22 | 23 | 24 | com.github.detro.ghostdriver 25 | phantomjsdriver 26 | 27 | 28 | junit 29 | junit 30 | 4.11 31 | test 32 | 33 | 34 | 35 | 36 | 37 | com.github.detro.ghostdriver 38 | phantomjsdriver 39 | 1.1.0 40 | 41 | 42 | org.seleniumhq.selenium 43 | selenium-remote-driver 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /chapter05/recipe01/src/test/java/phantomjs/cookbook/PhantomJSDriverTest.java: -------------------------------------------------------------------------------- 1 | package phantomjs.cookbook; 2 | 3 | import org.junit.Test; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.Keys; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.WebElement; 8 | import org.openqa.selenium.phantomjs.PhantomJSDriver; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * PhantomJS Cookbook 14 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 15 | * Recipe 1 | Running Selenium tests with PhantomJS and GhostDriver 16 | * 17 | * Alternate version. 18 | */ 19 | public class PhantomJSDriverTest { 20 | 21 | private static final String THE_TEXT = "PhantomJS + GhostDriver"; 22 | 23 | @Test 24 | public void testPhantomJSDriver() { 25 | WebDriver driver = new PhantomJSDriver(); 26 | 27 | driver.get("http://localhost:3000/input-demo"); 28 | 29 | WebElement demo = driver.findElement(By.id("demo")); 30 | 31 | demo.sendKeys(THE_TEXT); 32 | demo.sendKeys(Keys.ENTER); 33 | 34 | WebElement stage = driver.findElement(By.id("stage")); 35 | 36 | final String stageText = stage.getText(); 37 | 38 | assertEquals(THE_TEXT, stageText); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /chapter05/recipe01/src/test/java/phantomjs/cookbook/RemoteWebDriverTest.java: -------------------------------------------------------------------------------- 1 | package phantomjs.cookbook; 2 | 3 | import org.junit.Test; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.Keys; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.WebElement; 8 | import org.openqa.selenium.remote.DesiredCapabilities; 9 | import org.openqa.selenium.remote.RemoteWebDriver; 10 | 11 | import java.net.URL; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | /** 16 | * PhantomJS Cookbook 17 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 18 | * Recipe 1 | Running Selenium tests with PhantomJS and GhostDriver 19 | */ 20 | public class RemoteWebDriverTest { 21 | 22 | private static final String THE_TEXT = "PhantomJS + GhostDriver"; 23 | 24 | // Don't forget to have a PhantomJS instance running: 25 | // $ phantomjs --webdriver=4444 26 | 27 | @Test 28 | public void testGhostDriver() throws Exception { 29 | WebDriver driver = new RemoteWebDriver( 30 | new URL("http://localhost:4444/"), 31 | DesiredCapabilities.phantomjs()); 32 | 33 | driver.get("http://localhost:3000/input-demo"); 34 | 35 | WebElement demo = driver.findElement(By.id("demo")); 36 | 37 | demo.sendKeys(THE_TEXT); 38 | demo.sendKeys(Keys.ENTER); 39 | 40 | WebElement stage = driver.findElement(By.id("stage")); 41 | 42 | final String stageText = stage.getText(); 43 | 44 | assertEquals(THE_TEXT, stageText); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter05/recipe02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomjs-cookbook-chapter05-recipe02", 3 | "description": "Selenium demos for recipe #2 in Chapter 5 of 'The PhantomJS Cookbook'.", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "selenium-webdriver": "~2.39.0", 7 | "webdriverjs": "~1.3.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter05/recipe02/selenium-webdriver-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 4 | * Recipe 2 | Using WebdriverJS as a Selenium client for PhantomJS 5 | * 6 | * Alternate version. 7 | */ 8 | var assert = require('assert'), 9 | test = require('selenium-webdriver/testing'), 10 | webdriver = require('selenium-webdriver'), 11 | driver = new webdriver.Builder() 12 | .withCapabilities(webdriver.Capabilities.phantomjs()) 13 | .build(); 14 | 15 | test.describe('/input-demo', function() { 16 | test.it('gets input from #demo and puts it onto #stage', function() { 17 | var THE_TEXT = 'PhantomJS + GhostDriver'; 18 | 19 | driver.get('http://localhost:3000/input-demo'); 20 | driver.findElement(webdriver.By.id('demo')).sendKeys(THE_TEXT); 21 | driver.findElement(webdriver.By.id('demo')).sendKeys(webdriver.Key.ENTER); 22 | 23 | driver.findElement(webdriver.By.id('stage')).getText().then(function(text) { 24 | assert(text === THE_TEXT, '#stage innerText equals ' + THE_TEXT); 25 | }); 26 | 27 | driver.quit(); 28 | }); 29 | }); -------------------------------------------------------------------------------- /chapter05/recipe02/webdriverjs-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 4 | * Recipe 2 | Using WebdriverJS as a Selenium client for PhantomJS 5 | */ 6 | var assert = require('assert'), 7 | driver = require('webdriverjs'), 8 | client, 9 | THE_TEXT = 'PhantomJS + GhostDriver'; 10 | 11 | describe('/input-demo', function() { 12 | beforeEach(function(done) { 13 | client = driver.remote({ 14 | desiredCapabilities: { 15 | browserName: 'phantomjs' 16 | } 17 | }).init(); 18 | client.url('http://localhost:3000/input-demo', done); 19 | }); 20 | 21 | afterEach(function(done) { 22 | client.end(done); 23 | }); 24 | 25 | it('gets input from #demo and puts it onto #stage', function(done) { 26 | client 27 | .setValue('#demo', THE_TEXT + '\uE007') 28 | .getText('#stage', function(err, text) { 29 | assert(text === THE_TEXT, '#stage innerText equals ' + THE_TEXT); 30 | }) 31 | .call(done); 32 | }); 33 | }); -------------------------------------------------------------------------------- /chapter05/recipe03.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'capybara' 3 | require 'capybara/dsl' 4 | require 'capybara/poltergeist' 5 | 6 | Capybara.run_server = false 7 | Capybara.default_driver = :poltergeist 8 | Capybara.app_host = 'http://localhost:3000' 9 | 10 | THE_TEXT = 'PhantomJS + Capybara + Poltergeist' 11 | 12 | # 13 | # PhantomJS Cookbook 14 | # Chapter 5 | Functional and End-to-end Testing with PhantomJS 15 | # Recipe 3 | Adding Poltergeist to a Capybara suite 16 | # 17 | module CookbookCapybaraDemo 18 | class Demo 19 | include Capybara::DSL 20 | def test_input_demo 21 | visit '/input-demo' 22 | fill_in 'demo', :with => THE_TEXT 23 | find('#demo').native.send_key(:Enter) 24 | find('#stage').text 25 | end 26 | end 27 | end 28 | 29 | demo = CookbookCapybaraDemo::Demo.new 30 | text = demo.test_input_demo 31 | 32 | puts "=> input '#{THE_TEXT}' and #stage received '#{text}' (same = #{text == THE_TEXT})" -------------------------------------------------------------------------------- /chapter05/recipe04.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'capybara' 3 | require 'capybara/dsl' 4 | require 'capybara/poltergeist' 5 | 6 | Capybara.run_server = false 7 | Capybara.default_driver = :poltergeist 8 | Capybara.app_host = 'http://localhost:3000' 9 | 10 | OUTPUT_STRING = "=> Captured as '%s'" 11 | RESOURCE = '/css-demo' 12 | SCREENSHOT_NAME = 'chapter05-recipe04%s.png' 13 | 14 | # 15 | # PhantomJS Cookbook 16 | # Chapter 5 | Functional and End-to-end Testing with PhantomJS 17 | # Recipe 4 | Taking screenshots during tests with Poltergeist 18 | # 19 | module CookbookCapybaraDemo 20 | class Demo 21 | include Capybara::DSL 22 | def capture_viewport 23 | page.driver.resize 1280, 1024 24 | visit RESOURCE 25 | screenshot_name = SCREENSHOT_NAME % ['-viewport'] 26 | save_screenshot(screenshot_name) 27 | puts OUTPUT_STRING % [screenshot_name] 28 | end 29 | def capture_full_page 30 | page.driver.resize 1280, 1024 31 | visit RESOURCE 32 | screenshot_name = SCREENSHOT_NAME % ['-fullpage'] 33 | save_screenshot(screenshot_name, :full => true) 34 | puts OUTPUT_STRING % [screenshot_name] 35 | end 36 | def capture_element(selector = 'body') 37 | visit RESOURCE 38 | screenshot_name = SCREENSHOT_NAME % ["-#{selector.gsub /\W/, ''}"] 39 | save_screenshot(screenshot_name, :selector => selector) 40 | puts OUTPUT_STRING % [screenshot_name] 41 | end 42 | def capture_after_scroll(distance = 500) 43 | page.driver.resize 1280, 1024 44 | visit RESOURCE 45 | page.driver.scroll_to(0, distance) 46 | # page.execute_script "window.scrollBy(0,#{distance})" 47 | screenshot_name = SCREENSHOT_NAME % ["-scroll-#{distance}"] 48 | save_screenshot(screenshot_name) 49 | puts OUTPUT_STRING % [screenshot_name] 50 | end 51 | end 52 | end 53 | 54 | demo = CookbookCapybaraDemo::Demo.new 55 | demo.capture_viewport -------------------------------------------------------------------------------- /chapter05/recipe05.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'capybara' 3 | require 'capybara/dsl' 4 | require 'capybara/poltergeist' 5 | 6 | Capybara.run_server = false 7 | Capybara.default_driver = :poltergeist 8 | Capybara.app_host = 'http://localhost:3000' 9 | 10 | # 11 | # PhantomJS Cookbook 12 | # Chapter 5 | Functional and End-to-end Testing with PhantomJS 13 | # Recipe 5 | Simulating precise mouse clicks with Poltergeist 14 | # 15 | module CookbookCapybaraDemo 16 | class Demo 17 | include Capybara::DSL 18 | def precise_click 19 | page.driver.resize 1280, 1024 20 | visit '/precision-click' 21 | page.driver.click 1280 - 21, 1024 - 21 22 | end 23 | end 24 | end 25 | 26 | demo = CookbookCapybaraDemo::Demo.new 27 | demo.precise_click -------------------------------------------------------------------------------- /chapter05/recipe07.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 4 | * Recipe 7 | Interacting with web pages using CasperJS 5 | */ 6 | var casper = require('casper').create(); 7 | 8 | casper.start('http://localhost:3000/', function() { 9 | this.clickLabel('/input-demo', 'a'); 10 | }); 11 | 12 | casper.then(function() { 13 | this.sendKeys('#demo', 'PhantomJS + CasperJS', {keepFocus: true}); 14 | this.sendKeys('#demo', casper.page.event.key.Enter, {keepFocus: true}); 15 | 16 | this.echo('#stage text is:'); 17 | this.echo(this.getHTML('#stage')); 18 | }); 19 | 20 | casper.run(); -------------------------------------------------------------------------------- /chapter05/recipe08.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 4 | * Recipe 8 | End-to-end testing with CasperJS 5 | */ 6 | casper.test.begin('Chapter 5 : Recipe #8', function(test) { 7 | var THE_TEXT = 'PhantomJS + CasperJS Testing'; 8 | 9 | casper 10 | .start('http://localhost:3000/', function() { 11 | test.assertExists('[href="/input-demo"]'); 12 | 13 | this.clickLabel('/input-demo', 'a'); 14 | }) 15 | .then(function() { 16 | var getDemoValue = (function() { 17 | return this.evaluate(function() { 18 | return __utils__.getFieldValue('demo'); 19 | }); 20 | }).bind(this); 21 | 22 | test.assertEquals(getDemoValue(), '', 23 | '#demo begins with no value set'); 24 | test.assertSelectorHasText('#stage', '', 25 | '#stage begins with no text'); 26 | 27 | this.sendKeys('#demo', THE_TEXT, {keepFocus: true}); 28 | test.assertEquals(getDemoValue(), THE_TEXT, 29 | 'value of #demo equals "' + THE_TEXT + '"'); 30 | 31 | this.sendKeys('#demo', casper.page.event.key.Enter, {keepFocus: true}); 32 | test.assertSelectorHasText('#stage', THE_TEXT, 33 | 'innerHTML of #stage equals "' + THE_TEXT + '"'); 34 | }) 35 | .run(function() { 36 | test.done(); 37 | }); 38 | }); -------------------------------------------------------------------------------- /chapter05/recipe10.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 5 | Functional and End-to-end Testing with PhantomJS 4 | * Recipe 10 | Detecting visual regressions using PhantomCSS 5 | */ 6 | var phantomcss = require('./../lib/phantomcss/phantomcss.js'); 7 | 8 | phantomcss.init({ 9 | libraryRoot: './lib/phantomcss' 10 | }); 11 | 12 | casper 13 | .start('http://localhost:3000/css-demo') 14 | .viewport(1280, 1024) 15 | .then(function() { 16 | phantomcss.screenshot('.jumbotron', 'jumbotron'); 17 | phantomcss.compareAll(); 18 | }) 19 | .run(function(){ 20 | casper.test.done(); 21 | phantom.exit(phantomcss.getExitStatus()); 22 | }); 23 | -------------------------------------------------------------------------------- /chapter06/recipe06.js: -------------------------------------------------------------------------------- 1 | /*global YSLOW */ 2 | /** 3 | * PhantomJS Cookbook 4 | * Chapter 6 | Network Monitoring and Performance Analysis 5 | * Recipe 6 | Executing a YSlow performance analysis with a custom ruleset 6 | */ 7 | YSLOW.registerRuleset({ 8 | id: 'cookbook', 9 | name: 'PhantomJS Cookbook Example Ruleset', 10 | rules: { 11 | ynumreq: {}, 12 | yexpires: {}, 13 | ycompress: {}, 14 | ycsstop: {}, 15 | yjsbottom: {}, 16 | ydupes: {}, 17 | ymindom: {}, 18 | yno404: {}, 19 | yemptysrc: {} 20 | }, 21 | weights: { 22 | ynumreq: 8, 23 | yexpires: 2, 24 | ycompress: 4, 25 | ycsstop: 2, 26 | yjsbottom: 2, 27 | ydupes: 4, 28 | ymindom: 2, 29 | yno404: 2, 30 | yemptysrc: 4 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /chapter07/recipe01.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 1 | Rendering images from PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | filename = 'index.png'; 8 | 9 | webpage.viewportSize = { width: 1024, height: 384 }; 10 | 11 | webpage.open('http://localhost:3000/', function(status) { 12 | if (status === 'fail') { 13 | console.error('webpage did not open successfully'); 14 | phantom.exit(1); 15 | } 16 | 17 | webpage.render(filename); 18 | 19 | console.log('webpage rendered as ' + filename); 20 | 21 | phantom.exit(); 22 | }); -------------------------------------------------------------------------------- /chapter07/recipe02.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 2 | Saving images as Base64 from PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | args = require('system').args, 8 | format = args[1] || 'jpeg'; 9 | 10 | webpage.viewportSize = { width: 1024, height: 768 }; 11 | 12 | webpage.open('http://localhost:3000/', function(status) { 13 | if (status === 'fail') { 14 | console.error('webpage did not open successfully'); 15 | phantom.exit(1); 16 | } 17 | 18 | console.log(webpage.renderBase64(format)); 19 | 20 | phantom.exit(); 21 | }); -------------------------------------------------------------------------------- /chapter07/recipe03.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 3 | Rendering and rasterizing SVGs from PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | filename = 'eyes.png'; 8 | 9 | webpage.open('http://localhost:3000/svg/eyes.svg', function(status) { 10 | if (status === 'fail') { 11 | console.error('webpage did not open successfully'); 12 | phantom.exit(1); 13 | } 14 | 15 | webpage.render(filename); 16 | 17 | console.log('webpage rendered as ' + filename); 18 | 19 | phantom.exit(); 20 | }); -------------------------------------------------------------------------------- /chapter07/recipe04-supplement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 4 | Generating clipped screenshots from PhantomJS 5 | * 6 | * Supplementary version. 7 | */ 8 | var webpage = require('webpage').create(), 9 | args = require('system').args, 10 | width = args[1], 11 | height = args[2], 12 | filename = width + 'px-X-' + height + 'px.png'; 13 | 14 | if (!width || !height) { 15 | console.error('viewport size was not specified'); 16 | phantom.exit(1); 17 | } 18 | 19 | webpage.viewportSize = { width: width, height: height }; 20 | 21 | webpage.clipRect = { 22 | top: 0, 23 | left: 0, 24 | width: width, 25 | height: height 26 | }; 27 | 28 | webpage.open('http://localhost:3000/', function(status) { 29 | if (status === 'fail') { 30 | console.error('webpage did not open successfully'); 31 | phantom.exit(1); 32 | } 33 | 34 | webpage.render(filename); 35 | phantom.exit(); 36 | }); -------------------------------------------------------------------------------- /chapter07/recipe04.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 4 | Generating clipped screenshots from PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | selector = require('system').args[1], 8 | filename; 9 | 10 | if (!selector) { 11 | console.error('no selector was specified'); 12 | phantom.exit(1); 13 | } 14 | 15 | filename = selector.replace(/\s/g, '-').replace(/\W/g, '') + '.png'; 16 | 17 | webpage.viewportSize = { width: 1024, height: 768 }; 18 | 19 | webpage.open('http://localhost:3000/', function(status) { 20 | if (status === 'fail') { 21 | console.error('webpage did not open successfully'); 22 | phantom.exit(1); 23 | } 24 | 25 | webpage.clipRect = webpage.evaluate(function(selector) { 26 | var el = document.querySelector(selector); 27 | return { 28 | top: el.offsetTop, 29 | left: el.offsetLeft, 30 | width: el.offsetWidth, 31 | height: el.offsetHeight 32 | }; 33 | }, selector); 34 | 35 | webpage.render(filename); 36 | 37 | console.log('webpage rendered as ' + filename); 38 | 39 | phantom.exit(); 40 | }); -------------------------------------------------------------------------------- /chapter07/recipe05.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 5 | Saving a web page from PhantomJS as a PDF 5 | */ 6 | var webpage = require('webpage').create(), 7 | filename = 'css-demo.pdf'; 8 | 9 | webpage.viewportSize = { width: 1024, height: 768 }; 10 | 11 | webpage.paperSize = { 12 | format: 'Letter', 13 | orientation: 'portrait', 14 | border: '0.5in' 15 | }; 16 | 17 | webpage.open('http://localhost:3000/css-demo', function(status) { 18 | if (status === 'fail') { 19 | console.error('webpage did not open successfully'); 20 | phantom.exit(1); 21 | } 22 | 23 | webpage.render(filename); 24 | 25 | console.log('webpage rendered as ' + filename); 26 | 27 | phantom.exit(); 28 | }); 29 | -------------------------------------------------------------------------------- /chapter07/recipe06.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 6 | Applying custom headers and footers to PDFs generated from PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | filename = 'css-demo.pdf', 8 | datetime = new Date().toString(), 9 | title; 10 | 11 | webpage.viewportSize = { width: 1024, height: 768 }; 12 | 13 | webpage.open('http://localhost:3000/css-demo', function(status) { 14 | if (status === 'fail') { 15 | console.error('webpage did not open successfully'); 16 | phantom.exit(1); 17 | } 18 | 19 | title = webpage.evaluate(function() { 20 | return document.querySelector('title').innerText; 21 | }); 22 | 23 | webpage.paperSize = { 24 | format: 'Letter', 25 | orientation: 'portrait', 26 | border: '0.5in', 27 | header: { 28 | height: '0.5in', 29 | contents: phantom.callback(function() { 30 | return '

' + 31 | title + '

'; 32 | }) 33 | }, 34 | footer: { 35 | height: '0.5in', 36 | contents: phantom.callback(function(pageNum, numPages) { 37 | return '
' + 38 | '
Rendered: ' + datetime + 39 | '
Pages: ' + 40 | pageNum + '/' + numPages + '
'; 41 | }) 42 | } 43 | }; 44 | 45 | webpage.render(filename); 46 | 47 | console.log('webpage rendered as ' + filename); 48 | 49 | phantom.exit(); 50 | }); 51 | -------------------------------------------------------------------------------- /chapter07/recipe07-casper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 7 | Testing responsive designs with PhantomJS 5 | * 6 | * CasperJS version. 7 | */ 8 | var casper = require('casper').create(), 9 | viewports = casper.cli.args.map(function(v) { 10 | return v.split(/x/i).map(Number); 11 | }), 12 | filename; 13 | 14 | function screenshot(vps) { 15 | var vp = vps.pop(); 16 | 17 | casper.viewport(vp[0], vp[1] || 600, function() { 18 | filename = vp.join('x') + '.png'; 19 | 20 | this.capture(filename); 21 | this.echo('webpage rendered as ' + filename); 22 | 23 | if(vps.length) screenshot(vps); 24 | }); 25 | } 26 | 27 | casper.start('http://localhost:3000/responsive-demo', function() { 28 | screenshot(viewports); 29 | }).run(); -------------------------------------------------------------------------------- /chapter07/recipe07-cliprect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 7 | Testing responsive designs with PhantomJS 5 | * 6 | * Alternate version with clipRect. 7 | */ 8 | var webpage = require('webpage').create(), 9 | args = require('system').args, 10 | viewports = args.slice(1).map(function(v) { 11 | return v.split(/x/i); 12 | }), 13 | filename; 14 | 15 | function screenshot(vps) { 16 | var vp = vps.pop(); 17 | 18 | webpage.viewportSize = { 19 | width: vp[0], 20 | height: vp[1] || 600 21 | }; 22 | 23 | webpage.clipRect = { 24 | left: 0, 25 | width: vp[0], 26 | top: 0, 27 | height: vp[1] || 600 28 | }; 29 | 30 | setTimeout(function() { 31 | filename = vp.join('x') + '.png'; 32 | 33 | webpage.render(filename); 34 | console.log('webpage rendered as ' + filename); 35 | 36 | vps.length > 0 ? screenshot(vps) : phantom.exit(); 37 | }, 50); 38 | } 39 | 40 | 41 | webpage.open('http://localhost:3000/responsive-demo', function(status) { 42 | if (status === 'fail') { 43 | console.error('webpage did not open successfully'); 44 | phantom.exit(1); 45 | } 46 | 47 | screenshot(viewports); 48 | }); 49 | -------------------------------------------------------------------------------- /chapter07/recipe07.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS Cookbook 3 | * Chapter 7 | Generating Images and Documents with PhantomJS 4 | * Recipe 7 | Testing responsive designs with PhantomJS 5 | */ 6 | var webpage = require('webpage').create(), 7 | args = require('system').args, 8 | viewports = args.slice(1).map(function(v) { 9 | return v.split(/x/i); 10 | }), 11 | filename; 12 | 13 | function screenshot(vps) { 14 | var vp = vps.pop(); 15 | webpage.viewportSize = { width: vp[0], height: vp[1] || 600 }; 16 | 17 | setTimeout(function() { 18 | filename = vp.join('x') + '.png'; 19 | 20 | webpage.render(filename); 21 | console.log('webpage rendered as ' + filename); 22 | 23 | vps.length > 0 ? screenshot(vps) : phantom.exit(); 24 | }, 50); 25 | } 26 | 27 | webpage.open('http://localhost:3000/responsive-demo', function(status) { 28 | if (status === 'fail') { 29 | console.error('webpage did not open successfully'); 30 | phantom.exit(1); 31 | } 32 | 33 | screenshot(viewports); 34 | }); 35 | -------------------------------------------------------------------------------- /chapter08/recipe-bonus-teamcity-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #4 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /chapter08/recipe01-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 37 | 38 | -------------------------------------------------------------------------------- /chapter08/recipe02-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | -------------------------------------------------------------------------------- /chapter08/recipe03-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The PhantomJS Cookbook - Chapter Four - Recipe #3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | -------------------------------------------------------------------------------- /lib/arg-parser.js: -------------------------------------------------------------------------------- 1 | /*global exports:true*/ 2 | function parseValue(v) { 3 | if (typeof v === 'undefined') { 4 | return true; 5 | } else { 6 | try { 7 | return JSON.parse(v); 8 | } catch (e) { 9 | return v; 10 | } 11 | } 12 | } 13 | 14 | function parseArguments(args) { 15 | return args.reduce(function(prev, current) { 16 | current = current.split('='); 17 | current[0] = current[0].replace(/^--/, ''); 18 | 19 | prev[current[0]] = parseValue(current[1]); 20 | 21 | return prev; 22 | }, {}); 23 | } 24 | 25 | /** 26 | * Parses arguments from the PhantomJS CLI. Use it like this: 27 | * ``` 28 | * var args = require('system').args.slice(1); 29 | * var argParser = require('./arg-parser'); 30 | * argParser.parseArgs(args); 31 | * ``` 32 | * @param args {Array} 33 | * @return parsedArgs {Object} 34 | */ 35 | exports.parseArgs = parseArguments; -------------------------------------------------------------------------------- /lib/hemingway.js: -------------------------------------------------------------------------------- 1 | // apologies to @angus-c for the blatant theft 2 | // https://github.com/angus-c/literaryJavaScript/ 3 | function fibonacci(size) { 4 | 5 | var first = 0, second = 1, next, count = 2, result = [first, second]; 6 | 7 | if(size < 2) 8 | return "the request was made but it was not good" 9 | 10 | while(count++ < size) { 11 | next = first + second; 12 | first = second; 13 | second = next; 14 | result.push(next); 15 | } 16 | return result; 17 | } -------------------------------------------------------------------------------- /lib/jasmine-reporters/load_reporters.js: -------------------------------------------------------------------------------- 1 | require("./jasmine.console_reporter.js") 2 | require("./jasmine.junit_reporter.js") 3 | require("./jasmine.nunit_reporter.js") 4 | require("./jasmine.teamcity_reporter.js") 5 | require("./jasmine.tap_reporter.js") 6 | -------------------------------------------------------------------------------- /lib/jasmine-reporters/phantomjs.runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # sanity check to make sure phantomjs exists in the PATH 4 | hash /usr/bin/env phantomjs &> /dev/null 5 | if [ $? -eq 1 ]; then 6 | echo "ERROR: phantomjs is not installed" 7 | echo "Please visit http://www.phantomjs.org/" 8 | exit 1 9 | fi 10 | 11 | # sanity check number of args 12 | if [ $# -lt 1 ] 13 | then 14 | echo "Usage: `basename $0` path_to_runner.html" 15 | echo 16 | exit 1 17 | fi 18 | 19 | SCRIPTDIR=$(dirname `perl -e 'use Cwd "abs_path";print abs_path(shift)' $0`) 20 | TESTFILE="" 21 | while (( "$#" )); do 22 | if [ ${1:0:7} == "http://" -o ${1:0:8} == "https://" ]; then 23 | TESTFILE="$TESTFILE $1" 24 | else 25 | TESTFILE="$TESTFILE `perl -e 'use Cwd "abs_path";print abs_path(shift)' $1`" 26 | fi 27 | shift 28 | done 29 | 30 | # cleanup previous test runs 31 | cd $SCRIPTDIR 32 | rm -f *.xml 33 | 34 | ## EXCLUDED FROM phantomjs-cookbook 35 | ## 36 | # # make sure phantomjs submodule is initialized 37 | # cd .. 38 | # git submodule update --init 39 | ## 40 | 41 | # fire up the phantomjs environment and run the test 42 | cd $SCRIPTDIR 43 | /usr/bin/env phantomjs $SCRIPTDIR/phantomjs-testrunner.js $TESTFILE 44 | -------------------------------------------------------------------------------- /lib/math-utils-spec.js: -------------------------------------------------------------------------------- 1 | describe('mathy', function() { 2 | 3 | describe('sum(args...)', function() { 4 | it('sums a variable number of arguments', function() { 5 | expect(mathy.sum(1, 1)).toBe(2); 6 | expect(mathy.sum(1, 2, 3)).toBe(6); 7 | // intentionally fail the following: 8 | expect(mathy.sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)).toBe(56); 9 | }); 10 | 11 | it('casts number-like objects to their corresponding numbers', function() { 12 | expect(mathy.sum('1', '2')).toBe(3); 13 | // intentionally fail the following: 14 | expect(mathy.sum(true, false)).toBe(0); 15 | }); 16 | 17 | it('throws an error when it encounters NaN', function() { 18 | expect(function() { 19 | mathy.sum('alpha', 'beta'); 20 | }).toThrow(); 21 | // intentionally fail the following: 22 | expect(function() { 23 | mathy.sum('01', '02'); 24 | }).toThrow(); 25 | }); 26 | }); 27 | 28 | describe('factorial(n)', function() { 29 | it('returns the factorial of n', function() { 30 | expect(mathy.factorial(1)).toBe(1); 31 | expect(mathy.factorial(2)).toBe(2); 32 | expect(mathy.factorial(3)).toBe(6); 33 | // intentionally fail the following: 34 | expect(mathy.factorial(10)).toBe(3628801); 35 | }); 36 | 37 | it('returns 1 when n < 2', function() { 38 | // regardless of whether that's actually true... 39 | expect(mathy.factorial(0)).toBe(1); 40 | expect(mathy.factorial(-1)).toBe(1); 41 | expect(mathy.factorial(-10)).toBe(1); 42 | }); 43 | 44 | it('throws an error when it encounters NaN', function() { 45 | expect(function() { 46 | mathy.factorial('alpha'); 47 | }).toThrow(); 48 | }); 49 | }); 50 | 51 | }); -------------------------------------------------------------------------------- /lib/math-utils.js: -------------------------------------------------------------------------------- 1 | var mathy = (function(mathy) { 2 | 3 | mathy.sum = function sum(/*args...*/) { 4 | var args = [].slice.call(arguments, 0); 5 | 6 | return args.reduce(function(p, c) { 7 | c = Number(c); 8 | 9 | if (isNaN(c)) { 10 | throw new TypeError(c + ' cannot be cast as a number.'); 11 | } 12 | 13 | return p + c; 14 | }, 0); 15 | }; 16 | 17 | mathy.factorial = function factorial(n) { 18 | if(isNaN(n)) throw new TypeError(n + ' is not a number.'); 19 | if(n < 2) return 1; 20 | return n * factorial(n - 1); 21 | }; 22 | 23 | return mathy; 24 | }({})); -------------------------------------------------------------------------------- /lib/phantomcss/ResembleJs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright © 2013 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 | -------------------------------------------------------------------------------- /lib/phantomcss/ResembleJs/README.md: -------------------------------------------------------------------------------- 1 | Resemble.js 2 | ========== 3 | 4 | Analyse and compare images with Javascript and HTML5. [Resemble.js Demo](http://huddle.github.com/Resemble.js/) 5 | 6 | ### Example 7 | 8 | Retrieve basic analysis on image. 9 | 10 | ```javascript 11 | var api = resemble(fileData).onComplete(function(data){ 12 | console.log(data); 13 | /* 14 | { 15 | red: 255, 16 | green: 255, 17 | blue: 255, 18 | brightness: 255 19 | } 20 | */ 21 | }); 22 | ``` 23 | 24 | Use resemble to compare two images. 25 | 26 | ```javascript 27 | var diff = resemble(file).compareTo(file2).ignoreColors().onComplete(function(data){ 28 | console.log(data); 29 | /* 30 | { 31 | misMatchPercentage : 100, // % 32 | isSameDimensions: true, // or false 33 | dimensionDifference: { width: 0, height: -1 }, // defined if dimensions are not the same 34 | getImageDataUrl: function(){} 35 | } 36 | */ 37 | }); 38 | ``` 39 | 40 | You can also change the comparison method after the first analysis. 41 | 42 | ```javascript 43 | // diff.ignoreNothing(); 44 | // diff.ignoreColors(); 45 | diff.ignoreAntialiasing(); 46 | ``` 47 | 48 | -------------------------------------- 49 | 50 | Created by [James Cryer](http://github.com/jamescryer) and the Huddle development team. -------------------------------------------------------------------------------- /lib/phantomcss/ResembleJs/resemblejscontainer.html: -------------------------------------------------------------------------------- 1 | This blank HTML page is used for processing the images with Resemble.js -------------------------------------------------------------------------------- /lib/string-utils-expectations.js: -------------------------------------------------------------------------------- 1 | describe('string-utils.js', function() { 2 | describe('txtr.capitalize(s)', function() { 3 | it('capitalizes the first letter', function() { 4 | expect(txtr.capitalize('foo')).to.equal('Foo'); 5 | expect(txtr.capitalize('Foo')).to.equal('Foo'); 6 | expect(txtr.capitalize('foo bar')).to.equal('Foo bar'); 7 | expect(txtr.capitalize('Foo Bar')).to.equal('Foo Bar'); 8 | expect(txtr.capitalize('')).to.equal(''); 9 | }); 10 | }); 11 | 12 | describe('txtr.dashedToCamel(s, ic)', function() { 13 | it('converts dashed-strings to camelCaseStrings', function() { 14 | expect(txtr.dashedToCamel('css-style-string')).to.equal('cssStyleString'); 15 | }); 16 | it('converts dashed-strings to CamelCaseStrings with an initial capital when specified', function() { 17 | expect(txtr.dashedToCamel('css-style-string', true)).to.equal('CssStyleString'); 18 | }); 19 | }); 20 | 21 | describe('txtr.format(s, /*...*/)', function() { 22 | function baz() { 23 | return 'baz'; 24 | } 25 | 26 | it('formats a string with strings', function() { 27 | expect(txtr.format('Foo {0}', 'Bar')).to.equal('Foo Bar'); 28 | }); 29 | it('formats a string with numbers', function() { 30 | expect(txtr.format('Foo {0}', 42)).to.equal('Foo 42'); 31 | }); 32 | it('formats a string with a function', function() { 33 | expect(txtr.format('Foo {0}', baz)).to.equal('Foo baz'); 34 | }); 35 | it('formats a string with a boolean', function() { 36 | expect(txtr.format('Foo {0}', true)).to.equal('Foo true'); 37 | }); 38 | it('formats a string with multiple items', function() { 39 | expect(txtr.format('Foo {0} {1} {2} {3}', 40 | 'Bar', 42, baz, true)).to.equal('Foo Bar 42 baz true'); 41 | }); 42 | it('returns the token itself when there are not enough arguments to substitute for them all', function() { 43 | expect(txtr.format('Foo {0} {1}', 'bar')).to.equal('Foo bar {1}'); 44 | }); 45 | it('returns the token itself when it cannot otherwise recognize or handle the substitution', function() { 46 | expect(txtr.format('Foo {0}', [])).to.equal('Foo {0}'); 47 | expect(txtr.format('Foo {0}', {})).to.equal('Foo {0}'); 48 | expect(txtr.format('Foo {0}', /.*/)).to.equal('Foo {0}'); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /lib/string-utils-spec.js: -------------------------------------------------------------------------------- 1 | describe('string-utils.js', function() { 2 | describe('txtr.capitalize(s)', function() { 3 | it('capitalizes the first letter', function() { 4 | expect(txtr.capitalize('foo')).toBe('Foo'); 5 | expect(txtr.capitalize('Foo')).toBe('Foo'); 6 | expect(txtr.capitalize('foo bar')).toBe('Foo bar'); 7 | expect(txtr.capitalize('Foo Bar')).toBe('Foo Bar'); 8 | expect(txtr.capitalize('')).toBe(''); 9 | }); 10 | }); 11 | 12 | describe('txtr.dashedToCamel(s, ic)', function() { 13 | it('converts dashed-strings to camelCaseStrings', function() { 14 | expect(txtr.dashedToCamel('css-style-string')).toBe('cssStyleString'); 15 | }); 16 | it('converts dashed-strings to CamelCaseStrings with an initial capital when specified', function() { 17 | expect(txtr.dashedToCamel('css-style-string', true)).toBe('CssStyleString'); 18 | }); 19 | }); 20 | 21 | describe('txtr.format(s, /*...*/)', function() { 22 | function baz() { 23 | return 'baz'; 24 | } 25 | 26 | it('formats a string with strings', function() { 27 | expect(txtr.format('Foo {0}', 'Bar')).toBe('Foo Bar'); 28 | }); 29 | it('formats a string with numbers', function() { 30 | expect(txtr.format('Foo {0}', 42)).toBe('Foo 42'); 31 | }); 32 | it('formats a string with a function', function() { 33 | expect(txtr.format('Foo {0}', baz)).toBe('Foo baz'); 34 | }); 35 | it('formats a string with a boolean', function() { 36 | expect(txtr.format('Foo {0}', true)).toBe('Foo true'); 37 | }); 38 | it('formats a string with multiple items', function() { 39 | expect(txtr.format('Foo {0} {1} {2} {3}', 40 | 'Bar', 42, baz, true)).toBe('Foo Bar 42 baz true'); 41 | }); 42 | it('returns the token itself when there are not enough arguments to substitute for them all', function() { 43 | expect(txtr.format('Foo {0} {1}', 'bar')).toBe('Foo bar {1}'); 44 | }); 45 | it('returns the token itself when it cannot otherwise recognize or handle the substitution', function() { 46 | expect(txtr.format('Foo {0}', [])).toBe('Foo {0}'); 47 | expect(txtr.format('Foo {0}', {})).toBe('Foo {0}'); 48 | expect(txtr.format('Foo {0}', /.*/)).toBe('Foo {0}'); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /lib/string-utils.js: -------------------------------------------------------------------------------- 1 | var txtr = (function(txtr) { 2 | function capitalize(s) { 3 | return s.slice(0, 1).toUpperCase() + s.slice(1); 4 | } 5 | 6 | txtr.capitalize = capitalize; 7 | 8 | txtr.dashedToCamel = function(s, ic) { 9 | s = s.split('-'); 10 | var camelStr = s.shift(); 11 | 12 | while (s.length) { 13 | camelStr += capitalize(s.shift()); 14 | } 15 | 16 | if (ic === true) { 17 | camelStr = capitalize(camelStr); 18 | } 19 | 20 | return camelStr; 21 | }; 22 | 23 | var FORMAT_RX = /\{(\d+)\}/g; 24 | txtr.format = function(s /*...*/) { 25 | var args = Array.prototype.slice.call(arguments, 1); 26 | return s.replace(FORMAT_RX, function(token, i){ 27 | switch(typeof args[i]){ 28 | case 'number': 29 | case 'string': 30 | return args[i]; 31 | case 'function': 32 | return args[i](); 33 | case 'boolean': 34 | return args[i] + ''; 35 | default: 36 | return token; 37 | } 38 | }); 39 | }; 40 | 41 | return txtr; 42 | }({})); 43 | -------------------------------------------------------------------------------- /phantomjs-sandbox/.bowerrc: -------------------------------------------------------------------------------- 1 | { "directory": "static/components" } -------------------------------------------------------------------------------- /phantomjs-sandbox/app.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | var express = require('express'), 3 | favicon = require('static-favicon'), 4 | logger = require('morgan'), 5 | bodyParser = require('body-parser'), 6 | methodOverride = require('method-override'), 7 | cookieParser = require('cookie-parser'), 8 | routes = require('./routes'), 9 | path = require('path'), 10 | 11 | app = express(), 12 | http = require('http').createServer(app), 13 | 14 | ws = require('websocket-driver'), 15 | 16 | views = require('./views-list').views; 17 | 18 | 19 | app.set('port', process.env.PORT || 3000); 20 | app.set('views', __dirname + '/views'); 21 | app.set('view engine', 'ejs'); 22 | app.use(favicon()); 23 | app.use(logger('dev')); 24 | app.use(bodyParser()); 25 | app.use(methodOverride()); 26 | app.use(cookieParser()); 27 | app.use(express.static(path.join(__dirname, 'static'), {maxAge: 86400000})); 28 | 29 | app.get('/', routes.index); 30 | 31 | // Routes: 32 | views.forEach(function(it) { 33 | app.get('/' + it, routes[it.replace(/-([a-z])/g, function(m) { return m[1].toUpperCase(); })]); 34 | }); 35 | 36 | // Chapter 3, Recipe 3 37 | app.post('/post-demo', routes.postDemo); 38 | 39 | // Chapter 3, Recipe 13 40 | app.get('/ajax-demo', routes.ajaxDemo); 41 | 42 | // Chapter 3, Recipe 14 43 | http.on('upgrade', function(request, socket, body) { 44 | if (!ws.isWebSocket(request)) return; 45 | 46 | var driver = ws.http(request); 47 | 48 | driver.io.write(body); 49 | socket.pipe(driver.io).pipe(socket); 50 | 51 | driver.messages.on('data', function(message) { 52 | console.log('[WebSocket] %s', message); 53 | 54 | setInterval(function() { 55 | var json = JSON.stringify({ 56 | type:'message', 57 | data: new Date().getTime() 58 | }); 59 | driver.text(json); 60 | }, 100); 61 | }); 62 | 63 | driver.start(); 64 | }); 65 | 66 | // Chapter 8, Recipe 5 67 | app.post('/form-demo', routes.formDemo); 68 | 69 | http.listen(app.get('port')); 70 | console.log('[phantomjs-sandbox] App is listening on %s.', app.get('port')); 71 | -------------------------------------------------------------------------------- /phantomjs-sandbox/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomjs-sandbox", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "bootstrap": "~3.0.3", 6 | "moment": "~2.5.1", 7 | "snap.svg": "~0.2.0" 8 | }, 9 | "devDependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /phantomjs-sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomjs-sandbox", 3 | "description": "A trivial demo app that demonstrates some of the PhantomJS features for 'PhantomJS Cookbook'.", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "express": "4.0.0", 7 | "body-parser": "1.0.2", 8 | "cookie-parser": "1.0.1", 9 | "static-favicon": "1.0.2", 10 | "morgan": "1.0.0", 11 | "method-override": "1.0.0", 12 | "ejs": "*", 13 | "lorem-ipsum": "~1.0.1", 14 | "websocket-driver": "~0.3.2" 15 | }, 16 | "private": true 17 | } 18 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "version": "3.0.3", 4 | "main": [ 5 | "./dist/js/bootstrap.js", 6 | "./dist/css/bootstrap.css", 7 | "./dist/fonts/glyphicons-halflings-regular.eot", 8 | "./dist/fonts/glyphicons-halflings-regular.svg", 9 | "./dist/fonts/glyphicons-halflings-regular.ttf", 10 | "./dist/fonts/glyphicons-halflings-regular.woff" 11 | ], 12 | "ignore": [ 13 | "**/.*", 14 | "_*", 15 | "docs-assets", 16 | "examples", 17 | "/fonts", 18 | "js/tests", 19 | "CNAME", 20 | "CONTRIBUTING.md", 21 | "Gruntfile.js", 22 | "browserstack.json", 23 | "composer.json", 24 | "package.json", 25 | "*.html" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.9.0" 29 | }, 30 | "homepage": "https://github.com/twbs/bootstrap", 31 | "_release": "3.0.3", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "v3.0.3", 35 | "commit": "6d03173a1aad98e75f7d33e65b411c519176c59a" 36 | }, 37 | "_source": "git://github.com/twbs/bootstrap.git", 38 | "_target": "~3.0.3", 39 | "_originalSource": "bootstrap", 40 | "_direct": true 41 | } -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Twitter, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "version": "3.0.3", 4 | "main": [ 5 | "./dist/js/bootstrap.js", 6 | "./dist/css/bootstrap.css", 7 | "./dist/fonts/glyphicons-halflings-regular.eot", 8 | "./dist/fonts/glyphicons-halflings-regular.svg", 9 | "./dist/fonts/glyphicons-halflings-regular.ttf", 10 | "./dist/fonts/glyphicons-halflings-regular.woff" 11 | ], 12 | "ignore": [ 13 | "**/.*", 14 | "_*", 15 | "docs-assets", 16 | "examples", 17 | "/fonts", 18 | "js/tests", 19 | "CNAME", 20 | "CONTRIBUTING.md", 21 | "Gruntfile.js", 22 | "browserstack.json", 23 | "composer.json", 24 | "package.json", 25 | "*.html" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.9.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/founddrama/phantomjs-cookbook/d7121cea1169f60313a2b1d871504908c8ddedc0/phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/founddrama/phantomjs-cookbook/d7121cea1169f60313a2b1d871504908c8ddedc0/phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/founddrama/phantomjs-cookbook/d7121cea1169f60313a2b1d871504908c8ddedc0/phantomjs-sandbox/static/components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/js/transition.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * Bootstrap: transition.js v3.0.3 3 | * http://getbootstrap.com/javascript/#transitions 4 | * ======================================================================== 5 | * Copyright 2013 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ======================================================================== */ 19 | 20 | 21 | +function ($) { "use strict"; 22 | 23 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 24 | // ============================================================ 25 | 26 | function transitionEnd() { 27 | var el = document.createElement('bootstrap') 28 | 29 | var transEndEventNames = { 30 | 'WebkitTransition' : 'webkitTransitionEnd' 31 | , 'MozTransition' : 'transitionend' 32 | , 'OTransition' : 'oTransitionEnd otransitionend' 33 | , 'transition' : 'transitionend' 34 | } 35 | 36 | for (var name in transEndEventNames) { 37 | if (el.style[name] !== undefined) { 38 | return { end: transEndEventNames[name] } 39 | } 40 | } 41 | } 42 | 43 | // http://blog.alexmaccaw.com/css-transitions 44 | $.fn.emulateTransitionEnd = function (duration) { 45 | var called = false, $el = this 46 | $(this).one($.support.transition.end, function () { called = true }) 47 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 48 | setTimeout(callback, duration) 49 | return this 50 | } 51 | 52 | $(function () { 53 | $.support.transition = transitionEnd() 54 | }) 55 | 56 | }(jQuery); 57 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/alerts.less: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: @alert-padding; 11 | margin-bottom: @line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: @alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing @headings-color 19 | color: inherit; 20 | } 21 | // Provide class for links that match alerts 22 | .alert-link { 23 | font-weight: @alert-link-font-weight; 24 | } 25 | 26 | // Improve alignment and spacing of inner content 27 | > p, 28 | > ul { 29 | margin-bottom: 0; 30 | } 31 | > p + p { 32 | margin-top: 5px; 33 | } 34 | } 35 | 36 | // Dismissable alerts 37 | // 38 | // Expand the right padding and account for the close button's positioning. 39 | 40 | .alert-dismissable { 41 | padding-right: (@alert-padding + 20); 42 | 43 | // Adjust close link position 44 | .close { 45 | position: relative; 46 | top: -2px; 47 | right: -21px; 48 | color: inherit; 49 | } 50 | } 51 | 52 | // Alternate styles 53 | // 54 | // Generate contextual modifier classes for colorizing the alert. 55 | 56 | .alert-success { 57 | .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); 58 | } 59 | .alert-info { 60 | .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); 61 | } 62 | .alert-warning { 63 | .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); 64 | } 65 | .alert-danger { 66 | .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); 67 | } 68 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/badges.less: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base classes 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: @font-size-small; 12 | font-weight: @badge-font-weight; 13 | color: @badge-color; 14 | line-height: @badge-line-height; 15 | vertical-align: baseline; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: @badge-bg; 19 | border-radius: @badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | } 32 | 33 | // Hover state, but only for links 34 | a.badge { 35 | &:hover, 36 | &:focus { 37 | color: @badge-link-hover-color; 38 | text-decoration: none; 39 | cursor: pointer; 40 | } 41 | } 42 | 43 | // Account for counters in navs 44 | a.list-group-item.active > .badge, 45 | .nav-pills > .active > a > .badge { 46 | color: @badge-active-color; 47 | background-color: @badge-active-bg; 48 | } 49 | .nav-pills > li > a > .badge { 50 | margin-left: 3px; 51 | } 52 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Core variables and mixins 2 | @import "variables.less"; 3 | @import "mixins.less"; 4 | 5 | // Reset 6 | @import "normalize.less"; 7 | @import "print.less"; 8 | 9 | // Core CSS 10 | @import "scaffolding.less"; 11 | @import "type.less"; 12 | @import "code.less"; 13 | @import "grid.less"; 14 | @import "tables.less"; 15 | @import "forms.less"; 16 | @import "buttons.less"; 17 | 18 | // Components 19 | @import "component-animations.less"; 20 | @import "glyphicons.less"; 21 | @import "dropdowns.less"; 22 | @import "button-groups.less"; 23 | @import "input-groups.less"; 24 | @import "navs.less"; 25 | @import "navbar.less"; 26 | @import "breadcrumbs.less"; 27 | @import "pagination.less"; 28 | @import "pager.less"; 29 | @import "labels.less"; 30 | @import "badges.less"; 31 | @import "jumbotron.less"; 32 | @import "thumbnails.less"; 33 | @import "alerts.less"; 34 | @import "progress-bars.less"; 35 | @import "media.less"; 36 | @import "list-group.less"; 37 | @import "panels.less"; 38 | @import "wells.less"; 39 | @import "close.less"; 40 | 41 | // Components w/ JavaScript 42 | @import "modals.less"; 43 | @import "tooltip.less"; 44 | @import "popovers.less"; 45 | @import "carousel.less"; 46 | 47 | // Utility classes 48 | @import "utilities.less"; 49 | @import "responsive-utilities.less"; 50 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: 8px 15px; 8 | margin-bottom: @line-height-computed; 9 | list-style: none; 10 | background-color: @breadcrumb-bg; 11 | border-radius: @border-radius-base; 12 | > li { 13 | display: inline-block; 14 | + li:before { 15 | content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space 16 | padding: 0 5px; 17 | color: @breadcrumb-color; 18 | } 19 | } 20 | > .active { 21 | color: @breadcrumb-active-color; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/close.less: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: (@font-size-base * 1.5); 9 | font-weight: @close-font-weight; 10 | line-height: 1; 11 | color: @close-color; 12 | text-shadow: @close-text-shadow; 13 | .opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: @close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | .opacity(.5); 21 | } 22 | 23 | // Additional properties for button version 24 | // iOS requires the button element instead of an anchor tag. 25 | // If you want the anchor version, it requires `href="#"`. 26 | button& { 27 | padding: 0; 28 | cursor: pointer; 29 | background: transparent; 30 | border: 0; 31 | -webkit-appearance: none; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/code.less: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: @font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: @code-color; 19 | background-color: @code-bg; 20 | white-space: nowrap; 21 | border-radius: @border-radius-base; 22 | } 23 | 24 | // Blocks of code 25 | pre { 26 | display: block; 27 | padding: ((@line-height-computed - 1) / 2); 28 | margin: 0 0 (@line-height-computed / 2); 29 | font-size: (@font-size-base - 1); // 14px to 13px 30 | line-height: @line-height-base; 31 | word-break: break-all; 32 | word-wrap: break-word; 33 | color: @pre-color; 34 | background-color: @pre-bg; 35 | border: 1px solid @pre-border-color; 36 | border-radius: @border-radius-base; 37 | 38 | // Account for some code outputs that place code tags in pre tags 39 | code { 40 | padding: 0; 41 | font-size: inherit; 42 | color: inherit; 43 | white-space: pre-wrap; 44 | background-color: transparent; 45 | border-radius: 0; 46 | } 47 | } 48 | 49 | // Enable scrollable blocks of code 50 | .pre-scrollable { 51 | max-height: @pre-scrollable-max-height; 52 | overflow-y: scroll; 53 | } 54 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/component-animations.less: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | .transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | &.in { 21 | display: block; 22 | } 23 | } 24 | .collapsing { 25 | position: relative; 26 | height: 0; 27 | overflow: hidden; 28 | .transition(height .35s ease); 29 | } 30 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/grid.less: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | // Set the container width, and override it for fixed navbars in media queries 6 | .container { 7 | .container-fixed(); 8 | 9 | @media (min-width: @screen-sm) { 10 | width: @container-sm; 11 | } 12 | @media (min-width: @screen-md) { 13 | width: @container-md; 14 | } 15 | @media (min-width: @screen-lg-min) { 16 | width: @container-lg; 17 | } 18 | } 19 | 20 | // mobile first defaults 21 | .row { 22 | .make-row(); 23 | } 24 | 25 | // Common styles for small and large grid columns 26 | .make-grid-columns(); 27 | 28 | 29 | // Extra small grid 30 | // 31 | // Columns, offsets, pushes, and pulls for extra small devices like 32 | // smartphones. 33 | 34 | .make-grid-columns-float(xs); 35 | .make-grid(@grid-columns, xs, width); 36 | .make-grid(@grid-columns, xs, pull); 37 | .make-grid(@grid-columns, xs, push); 38 | .make-grid(@grid-columns, xs, offset); 39 | 40 | 41 | // Small grid 42 | // 43 | // Columns, offsets, pushes, and pulls for the small device range, from phones 44 | // to tablets. 45 | 46 | @media (min-width: @screen-sm-min) { 47 | .make-grid-columns-float(sm); 48 | .make-grid(@grid-columns, sm, width); 49 | .make-grid(@grid-columns, sm, pull); 50 | .make-grid(@grid-columns, sm, push); 51 | .make-grid(@grid-columns, sm, offset); 52 | } 53 | 54 | 55 | // Medium grid 56 | // 57 | // Columns, offsets, pushes, and pulls for the desktop device range. 58 | 59 | @media (min-width: @screen-md-min) { 60 | .make-grid-columns-float(md); 61 | .make-grid(@grid-columns, md, width); 62 | .make-grid(@grid-columns, md, pull); 63 | .make-grid(@grid-columns, md, push); 64 | .make-grid(@grid-columns, md, offset); 65 | } 66 | 67 | 68 | // Large grid 69 | // 70 | // Columns, offsets, pushes, and pulls for the large desktop device range. 71 | 72 | @media (min-width: @screen-lg-min) { 73 | .make-grid-columns-float(lg); 74 | .make-grid(@grid-columns, lg, width); 75 | .make-grid(@grid-columns, lg, pull); 76 | .make-grid(@grid-columns, lg, push); 77 | .make-grid(@grid-columns, lg, offset); 78 | } 79 | 80 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/jumbotron.less: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding: @jumbotron-padding; 8 | margin-bottom: @jumbotron-padding; 9 | font-size: @jumbotron-font-size; 10 | font-weight: 200; 11 | line-height: (@line-height-base * 1.5); 12 | color: @jumbotron-color; 13 | background-color: @jumbotron-bg; 14 | 15 | h1, 16 | .h1 { 17 | line-height: 1; 18 | color: @jumbotron-heading-color; 19 | } 20 | p { 21 | line-height: 1.4; 22 | } 23 | 24 | .container & { 25 | border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container 26 | } 27 | 28 | .container { 29 | max-width: 100%; 30 | } 31 | 32 | @media screen and (min-width: @screen-sm-min) { 33 | padding-top: (@jumbotron-padding * 1.6); 34 | padding-bottom: (@jumbotron-padding * 1.6); 35 | 36 | .container & { 37 | padding-left: (@jumbotron-padding * 2); 38 | padding-right: (@jumbotron-padding * 2); 39 | } 40 | 41 | h1, 42 | .h1 { 43 | font-size: (@font-size-base * 4.5); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/labels.less: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: @label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // Add hover effects, but only for links 18 | &[href] { 19 | &:hover, 20 | &:focus { 21 | color: @label-link-hover-color; 22 | text-decoration: none; 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | // Empty labels collapse automatically (not available in IE8) 28 | &:empty { 29 | display: none; 30 | } 31 | 32 | // Quick fix for labels in buttons 33 | .btn & { 34 | position: relative; 35 | top: -1px; 36 | } 37 | } 38 | 39 | // Colors 40 | // Contextual variations (linked labels get darker on :hover) 41 | 42 | .label-default { 43 | .label-variant(@label-default-bg); 44 | } 45 | 46 | .label-primary { 47 | .label-variant(@label-primary-bg); 48 | } 49 | 50 | .label-success { 51 | .label-variant(@label-success-bg); 52 | } 53 | 54 | .label-info { 55 | .label-variant(@label-info-bg); 56 | } 57 | 58 | .label-warning { 59 | .label-variant(@label-warning-bg); 60 | } 61 | 62 | .label-danger { 63 | .label-variant(@label-danger-bg); 64 | } 65 | -------------------------------------------------------------------------------- /phantomjs-sandbox/static/components/bootstrap/less/list-group.less: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | // Base class 6 | // 7 | // Easily usable on