├── .gitignore ├── CONTRIBUTING.md ├── test └── fixtures │ ├── inject.html │ ├── inject.js │ ├── headers.html │ ├── basic.html │ ├── viewportSize.html │ └── headers_server.js ├── .travis.yml ├── .jshintrc ├── AUTHORS ├── package.json ├── appveyor.yml ├── LICENSE-MIT ├── CHANGELOG ├── Gruntfile.js ├── phantomjs └── main.js ├── README.md └── lib └── phantomjs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | .idea 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please see the [Contributing to grunt](http://gruntjs.com/contributing) guide for information on contributing to this project. 2 | -------------------------------------------------------------------------------- /test/fixtures/inject.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "0.10" 7 | - "0.12" 8 | - "4" 9 | - "5" 10 | - "6" 11 | - "iojs" 12 | 13 | matrix: 14 | fast_finish: true 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "immed": true, 7 | "latedef": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "node": true, 11 | "sub": true, 12 | "undef": true, 13 | "unused": true 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Send messages to the parent PhantomJS process via alert! Good times!! 4 | function sendMessage() { 5 | var args = [].slice.call(arguments); 6 | alert(JSON.stringify(args)); 7 | } 8 | 9 | sendMessage('test', 'injected'); 10 | sendMessage('done'); 11 | -------------------------------------------------------------------------------- /test/fixtures/headers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | "Cowboy" Ben Alman (http://benalman.com/) 2 | Jörn Zaefferer (http://bassistance.de/) 3 | Kyle Robinson Young (http://dontkry.com/) 4 | Tyler Kellen (http://goingslowly.com) 5 | Sindre Sorhus (http://sindresorhus.com) 6 | Vlad Filippov (http://vladfilippov.com/) 7 | Brian J. Dowling 8 | Chris Talkington 9 | Cymen Vig 10 | FG Ribreau 11 | Ghislain Seguin 12 | Ian Crowther 13 | Jared Stehler 14 | Jarrod Overson 15 | Kelly Miyashiro 16 | Nick Nisi 17 | Patrick Kettner 18 | Sébastien Cevey 19 | William Dibbern 20 | -------------------------------------------------------------------------------- /test/fixtures/viewportSize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/headers_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var express = require('express'); 5 | var port = 8075; 6 | var site = express(); 7 | 8 | site.get('*', function(req, res) { 9 | fs.readFile('./test/fixtures/headers.html', 'utf8', function (err, data) { 10 | if (err) { 11 | throw err; 12 | } 13 | var tmpl = data.replace(/<% headers %>/, JSON.stringify(req.headers)); 14 | res.write(tmpl); 15 | res.end(); 16 | }); 17 | }); 18 | 19 | site.listen(port); 20 | 21 | console.log('Listening on port ' + port); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-lib-phantomjs", 3 | "description": "Grunt and PhantomJS, sitting in a tree", 4 | "version": "1.1.0", 5 | "author": { 6 | "name": "Grunt Team", 7 | "url": "http://gruntjs.com/" 8 | }, 9 | "repository": "gruntjs/grunt-lib-phantomjs", 10 | "license": "MIT", 11 | "engines": { 12 | "node": ">=0.10.0" 13 | }, 14 | "scripts": { 15 | "test": "grunt jshint test" 16 | }, 17 | "dependencies": { 18 | "eventemitter2": "^0.4.9", 19 | "phantomjs-prebuilt": "^2.1.3", 20 | "rimraf": "^2.5.2", 21 | "semver": "^5.1.0", 22 | "temporary": "^0.0.8" 23 | }, 24 | "devDependencies": { 25 | "difflet": "^1.0.1", 26 | "express": "^4.11.2", 27 | "grunt": "^1.0.1", 28 | "grunt-contrib-jshint": "^1.0.0" 29 | }, 30 | "main": "lib/phantomjs", 31 | "files": [ 32 | "lib", 33 | "phantomjs" 34 | ], 35 | "appveyor_id": "69g3o5c5m0fyih9r" 36 | } 37 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | clone_depth: 10 2 | 3 | version: "{build}" 4 | 5 | # What combinations to test 6 | environment: 7 | matrix: 8 | - nodejs_version: "0.10" 9 | platform: x86 10 | - nodejs_version: "0.12" 11 | platform: x86 12 | - nodejs_version: "4" 13 | platform: x64 14 | - nodejs_version: "4" 15 | platform: x86 16 | - nodejs_version: "5" 17 | platform: x86 18 | - nodejs_version: "6" 19 | platform: x86 20 | 21 | install: 22 | - ps: Install-Product node $env:nodejs_version $env:platform 23 | - npm install 24 | 25 | test_script: 26 | # Output useful info for debugging 27 | - node --version && npm --version 28 | # We test multiple Windows shells because of prior stdout buffering issues 29 | # filed against Grunt. https://github.com/joyent/node/issues/3584 30 | - ps: "npm test # PowerShell" # Pass comment to PS for easier debugging 31 | - cmd: npm test 32 | 33 | build: off 34 | 35 | matrix: 36 | fast_finish: true 37 | 38 | cache: 39 | - node_modules -> package.json 40 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 "Cowboy" Ben Alman, contributors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.1.0: 2 | date: 2016-05-12 3 | changes: 4 | - Add onResourceError event handler 5 | - Add onResourceTimeout event handler 6 | v1.0.2: 7 | date: 2016-04-14 8 | changes: 9 | - Update dependencies. 10 | - Add API docs to `README` 11 | - Fix EBUSY error on Windows when deleting temp file 12 | - Set a default value to `options.killTimeout` 13 | v1.0.1: 14 | date: 2016-02-12 15 | changes: 16 | - switched package names to phantomjs-prebuilt in deps. 17 | v1.0.0: 18 | date: 2016-01-26 19 | changes: 20 | - Update to PhantomJS ^2.1.2 21 | v0.7.1: 22 | date: 2015-07-10 23 | changes: 24 | - Handle code 'null' in spawn code 25 | v0.7.0: 26 | date: 2015-02-20 27 | changes: 28 | - Bump `phantomjs` dependency. 29 | - Add a handler for `phantom.onError`. 30 | - Check for a pid rather than connected property of process before killing it. 31 | v0.6.0: 32 | date: 2014-03-28 33 | changes: 34 | - Allow "spawn" to be called without options. 35 | - Kill spawned phantomjs process on cleanup() in case something is keeping it open. 36 | - Added option for screenshots. 37 | v0.5.0: 38 | date: 2014-01-17 39 | changes: 40 | - /41 /49 merged 41 | v0.4.0: 42 | date: 2013-09-02 43 | changes: 44 | - removed phantom.exit on failures after first load /24 /18 45 | - return full request object for debugging /34 46 | v0.3.0: 47 | date: 2013-04-03 48 | changes: 49 | - bumped phantomjs to 1.9.0-1 50 | v0.1.0: 51 | date: 2012-10-05 52 | changes: 53 | - Work in progress. 54 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-lib-phantomjs 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman, contributors 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function(grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | jshint: { 16 | all: [ 17 | 'Gruntfile.js', 18 | 'lib/*.js', 19 | 'phantomjs/main.js', 20 | 'test/*.js' 21 | ], 22 | options: { 23 | jshintrc: '.jshintrc' 24 | } 25 | }, 26 | test: { 27 | basic: { 28 | options: { 29 | url: 'test/fixtures/basic.html', 30 | expected: [1, 2, 3, 4, 5, 6], 31 | test: function test(a, b, c) { 32 | if (!test.actual) { test.actual = []; } 33 | test.actual.push(a, b, c); 34 | } 35 | } 36 | }, 37 | inject: { 38 | options: { 39 | url: 'test/fixtures/inject.html', 40 | expected: 'injected', 41 | test: function test(msg) { 42 | test.actual = msg; 43 | }, 44 | phantomJSOptions: { 45 | inject: require('path').resolve('test/fixtures/inject.js') 46 | } 47 | } 48 | }, 49 | headers: { 50 | options: { 51 | url: 'http://localhost:8075', 52 | server: './test/fixtures/headers_server.js', 53 | expected: 'custom_header_567', 54 | test: function test(msg) { 55 | test.actual = msg; 56 | }, 57 | phantomJSOptions: { 58 | page: { 59 | customHeaders: { 60 | 'X-CUSTOM': 'custom_header_567' 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | viewportSize: { 67 | options: { 68 | url: 'test/fixtures/viewportSize.html', 69 | expected: [1366, 800], 70 | test: function test(a, b) { 71 | if (!test.actual) { test.actual = []; } 72 | test.actual.push(a, b); 73 | }, 74 | phantomJSOptions: { 75 | page: { 76 | viewportSize: { 77 | width: 1366, 78 | height: 800 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | }); 86 | 87 | // The most basic of tests. Not even remotely comprehensive. 88 | grunt.registerMultiTask('test', 'A test, of sorts.', function() { 89 | var options = this.options(); 90 | var phantomjs = require('./lib/phantomjs').init(grunt); 91 | 92 | // Load up and Instantiate the test server 93 | if (options.server) { require(options.server); } 94 | 95 | // Do something. 96 | phantomjs.on('test', options.test); 97 | 98 | phantomjs.on('done', phantomjs.halt); 99 | 100 | phantomjs.on('debug', function(msg) { 101 | grunt.log.writeln('debug:' + msg); 102 | }); 103 | 104 | // Built-in error handlers. 105 | phantomjs.on('fail.load', function(url) { 106 | phantomjs.halt(); 107 | grunt.verbose.write('Running PhantomJS...').or.write('...'); 108 | grunt.log.error(); 109 | grunt.warn('PhantomJS unable to load "' + url + '" URI.'); 110 | }); 111 | 112 | phantomjs.on('fail.timeout', function() { 113 | phantomjs.halt(); 114 | grunt.log.writeln(); 115 | grunt.warn('PhantomJS timed out.'); 116 | }); 117 | 118 | // This task is async. 119 | var done = this.async(); 120 | 121 | // Spawn phantomjs 122 | phantomjs.spawn(options.url, { 123 | // Additional PhantomJS options. 124 | options: options.phantomJSOptions, 125 | // Complete the task when done. 126 | done: function(err) { 127 | if (err) { done(err); return; } 128 | var assert = require('assert'); 129 | var difflet = require('difflet')({indent: 2, comment: true}); 130 | try { 131 | assert.deepEqual(options.test.actual, options.expected, 'Actual should match expected.'); 132 | grunt.log.writeln('Test passed.'); 133 | done(); 134 | } catch (err) { 135 | grunt.log.subhead('Assertion Failure'); 136 | console.log(difflet.compare(err.expected, err.actual)); 137 | done(err); 138 | } 139 | } 140 | }); 141 | }); 142 | 143 | // The jshint plugin is used for linting. 144 | grunt.loadNpmTasks('grunt-contrib-jshint'); 145 | 146 | // By default, lint library. 147 | grunt.registerTask('default', ['jshint', 'test']); 148 | 149 | }; 150 | -------------------------------------------------------------------------------- /phantomjs/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-lib-phantomjs 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman, contributors 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | /* jshint phantom:true */ 10 | 11 | 'use strict'; 12 | 13 | var fs = require('fs'); 14 | var system = require('system'); 15 | 16 | // The temporary file used for communications. 17 | var tmpfile = system.args[1]; 18 | // The page .html file to load. 19 | var url = system.args[2]; 20 | // Extra, optionally overridable stuff. 21 | var options = JSON.parse(system.args[3] || {}); 22 | 23 | // Default options. 24 | if (!options.timeout) { options.timeout = 5000; } 25 | 26 | // Keep track of the last time a client message was sent. 27 | var last = new Date(); 28 | 29 | // Messages are sent to the parent by appending them to the tempfile. 30 | var sendMessage = function(arg) { 31 | var args = Array.isArray(arg) ? arg : [].slice.call(arguments); 32 | last = new Date(); 33 | fs.write(tmpfile, JSON.stringify(args) + '\n', 'a'); 34 | }; 35 | 36 | // This allows grunt to abort if the PhantomJS version isn't adequate. 37 | sendMessage('private', 'version', phantom.version); 38 | 39 | // Create a new page. 40 | var page = require('webpage').create(options.page); 41 | 42 | // Abort if the page doesn't send any messages for a while. 43 | setInterval(function() { 44 | if (new Date() - last > options.timeout) { 45 | sendMessage('fail.timeout'); 46 | if (options.screenshot) { 47 | page.render(['page-at-timeout-', Date.now(), '.jpg'].join('')); 48 | } 49 | phantom.exit(); 50 | } 51 | }, 100); 52 | 53 | 54 | // Inject bridge script into client page. 55 | var injected; 56 | var inject = function() { 57 | if (injected) { return; } 58 | // Inject client-side helper script. 59 | var scripts = Array.isArray(options.inject) ? options.inject : [options.inject]; 60 | sendMessage('inject', options.inject); 61 | scripts.forEach(page.injectJs); 62 | injected = true; 63 | }; 64 | 65 | // Keep track if the client-side helper script already has been injected. 66 | page.onUrlChanged = function(newUrl) { 67 | injected = false; 68 | sendMessage('onUrlChanged', newUrl); 69 | }; 70 | 71 | // The client page must send its messages via alert(jsonstring). 72 | page.onAlert = function(str) { 73 | // The only thing that should ever alert "inject" is the custom event 74 | // handler this script adds to be executed on DOMContentLoaded. 75 | if (str === 'inject') { 76 | inject(); 77 | return; 78 | } 79 | // Otherwise, parse the specified message string and send it back to grunt. 80 | // Unless there's a parse error. Then, complain. 81 | try { 82 | sendMessage(JSON.parse(str)); 83 | } catch (err) { 84 | sendMessage('error.invalidJSON', str); 85 | } 86 | }; 87 | 88 | // Relay console logging messages. 89 | page.onConsoleMessage = function(message) { 90 | sendMessage('console', message); 91 | }; 92 | 93 | // For debugging. 94 | page.onResourceRequested = function(request) { 95 | sendMessage('onResourceRequested', request); 96 | }; 97 | 98 | page.onResourceReceived = function(request) { 99 | if (request.stage === 'end') { 100 | sendMessage('onResourceReceived', request); 101 | } 102 | }; 103 | 104 | page.onError = function(msg, trace) { 105 | sendMessage('error.onError', msg, trace); 106 | }; 107 | 108 | phantom.onError = function(msg, trace) { 109 | sendMessage('error.onError', msg, trace); 110 | }; 111 | 112 | // Run before the page is loaded. 113 | page.onInitialized = function() { 114 | sendMessage('onInitialized'); 115 | // Abort if there is no bridge to inject. 116 | if (!options.inject) { return; } 117 | // Tell the client that when DOMContentLoaded fires, it needs to tell this 118 | // script to inject the bridge. This should ensure that the bridge gets 119 | // injected before any other DOMContentLoaded or window.load event handler. 120 | page.evaluate(function() { 121 | /* jshint browser:true, devel:true */ 122 | document.addEventListener('DOMContentLoaded', function() { 123 | alert('inject'); 124 | }, false); 125 | }); 126 | }; 127 | 128 | // Run when the page has finished loading. 129 | page.onLoadFinished = function(status) { 130 | // reset this handler to a no-op so further calls to onLoadFinished from iframes don't affect us 131 | page.onLoadFinished = function() { /* no-op */}; 132 | 133 | // The window has loaded. 134 | sendMessage('onLoadFinished', status); 135 | if (status !== 'success') { 136 | // File loading failure. 137 | sendMessage('fail.load', url); 138 | if (options.screenshot) { 139 | page.render(['page-at-timeout-', Date.now(), '.jpg'].join('')); 140 | } 141 | phantom.exit(); 142 | } 143 | }; 144 | 145 | page.onResourceError = function(resourceError) { 146 | sendMessage('error.onResourceError', resourceError); 147 | }; 148 | 149 | page.onResourceTimeout = function(request) { 150 | sendMessage('error.onResourceTimeout', request); 151 | }; 152 | 153 | // Actually load url. 154 | page.open(url); 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-lib-phantomjs [![Build Status: Linux](https://travis-ci.org/gruntjs/grunt-lib-phantomjs.svg?branch=master)](https://travis-ci.org/gruntjs/grunt-lib-phantomjs) [![Build Status: Windows](https://ci.appveyor.com/api/projects/status/69g3o5c5m0fyih9r/branch/master?svg=true)](https://ci.appveyor.com/project/gruntjs/grunt-lib-phantomjs/branch/master) 2 | 3 | > Grunt and PhantomJS, sitting in a tree. 4 | 5 | 6 | ## Usage 7 | 8 | The best way to understand how this lib should be used is by looking at the [grunt-contrib-qunit](https://github.com/gruntjs/grunt-contrib-qunit) plugin. Mainly, look at how [the lib is required](https://github.com/gruntjs/grunt-contrib-qunit/blob/d99291713d32f84e50303d6e51eb2dab40b1deb6/tasks/qunit.js#L17), how [event handlers are bound](https://github.com/gruntjs/grunt-contrib-qunit/blob/d99291713d32f84e50303d6e51eb2dab40b1deb6/tasks/qunit.js#L61-L144) and how [PhantomJS is actually spawned](https://github.com/gruntjs/grunt-contrib-qunit/blob/d99291713d32f84e50303d6e51eb2dab40b1deb6/tasks/qunit.js#L177-L190). 9 | 10 | Also, in the case of the grunt-contrib-qunit plugin, it's important to know that the page being loaded into PhantomJS *doesn't* know it will be loaded into PhantomJS, and as such doesn't have any PhantomJS->Grunt code in it. That communication code, aka. the ["bridge"](https://github.com/gruntjs/grunt-contrib-qunit/blob/d99291713d32f84e50303d6e51eb2dab40b1deb6/phantomjs/bridge.js), is dynamically [injected into the html page](https://github.com/gruntjs/grunt-contrib-qunit/blob/d99291713d32f84e50303d6e51eb2dab40b1deb6/tasks/qunit.js#L152). 11 | 12 | 13 | ## An inline example 14 | 15 | If a Grunt task looked something like this: 16 | 17 | ```js 18 | grunt.registerTask('mytask', 'Integrate with phantomjs.', function() { 19 | var phantomjs = require('grunt-lib-phantomjs').init(grunt); 20 | var errorCount = 0; 21 | 22 | // Handle any number of namespaced events like so. 23 | phantomjs.on('mytask.ok', function(msg) { 24 | grunt.log.writeln(msg); 25 | }); 26 | 27 | phantomjs.on('mytask.error', function(msg) { 28 | errorCount++; 29 | grunt.log.error(msg); 30 | }); 31 | 32 | // Create some kind of "all done" event. 33 | phantomjs.on('mytask.done', function() { 34 | phantomjs.halt(); 35 | }); 36 | 37 | // Built-in error handlers. 38 | phantomjs.on('fail.load', function(url) { 39 | phantomjs.halt(); 40 | grunt.warn('PhantomJS unable to load URL.'); 41 | }); 42 | 43 | phantomjs.on('fail.timeout', function() { 44 | phantomjs.halt(); 45 | grunt.warn('PhantomJS timed out.'); 46 | }); 47 | 48 | // This task is async. 49 | var done = this.async(); 50 | 51 | // Spawn phantomjs 52 | phantomjs.spawn('test.html', { 53 | // Additional PhantomJS options. 54 | options: {}, 55 | // Complete the task when done. 56 | done: function(err) { 57 | done(err || errorCount === 0); 58 | } 59 | }); 60 | 61 | }); 62 | ``` 63 | 64 | And `test.html` looked something like this (note the "bridge" is hard-coded into this page and not injected): 65 | 66 | ```html 67 | 68 | 69 | 70 | 83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | Then running Grunt would behave something like this: 90 | 91 | ```shell 92 | $ grunt mytask 93 | Running "mytask" task 94 | Something worked. 95 | >> Something failed. 96 | Warning: Task "mytask" failed. Use --force to continue. 97 | 98 | Aborted due to warnings. 99 | ``` 100 | 101 | 102 | ## API 103 | 104 | ### phantomjs.halt() 105 | 106 | Call this when everything has finished successfully, or when something horrible happens, and you need to clean up and abort. 107 | 108 | ### phantomjs.spawn(pageURL, options) 109 | 110 | Spawn a `PhantomJS` process. The method returns a reference to the spawned process. 111 | This method has the following arguments: 112 | 113 | #### pageURL 114 | 115 | Type: `string` 116 | Default: no default value, the user has to set it explicitly. 117 | 118 | URL or path to the page .html test file to run. 119 | 120 | #### Options 121 | 122 | Type: `object` 123 | 124 | The options object has these possible properties: 125 | 126 | ##### done 127 | 128 | Type: `function` 129 | Default: no default value, the user has to set it explicitly. 130 | 131 | The callback to call when the task is done. 132 | 133 | ##### failCode 134 | 135 | Type: `number` 136 | Default: 0 137 | 138 | The error code to exit with when an Error occurs. 139 | 140 | ##### killTimeout 141 | 142 | Type: `number` 143 | Default: `1000` ms 144 | 145 | The timeout in milliseconds after which the PhantomJS process will be killed. 146 | 147 | ##### options (PhantomJS options) 148 | 149 | Type: `object` 150 | Default: `{}` 151 | 152 | Additional options to passe to `PhantomJS`. This object has the following properties: 153 | 154 | ###### timeout 155 | 156 | Type: `number` 157 | Default: `undefined` 158 | 159 | PhantomJS' timeout, in milliseconds. 160 | 161 | ###### inject 162 | 163 | Type: `string|array` 164 | Default: `undefined` 165 | 166 | One or multiple (array) JavaScript file names to inject into the page. 167 | 168 | ###### page 169 | 170 | Type: `object` 171 | Default: `undefined` 172 | 173 | An object of options for the PhantomJS [`page` object](https://github.com/ariya/phantomjs/wiki/API-Reference-WebPage). 174 | 175 | ###### screenshot 176 | 177 | Type: `boolean` 178 | Default: `undefined` 179 | 180 | Saves a screenshot on failure 181 | 182 | 183 | ## OS Dependencies 184 | 185 | PhantomJS requires these dependencies on Ubuntu/Debian: 186 | 187 | ``` 188 | apt-get install libfontconfig1 fontconfig libfontconfig1-dev libfreetype6-dev 189 | ``` 190 | -------------------------------------------------------------------------------- /lib/phantomjs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-lib-phantomjs 3 | * http://gruntjs.com/ 4 | * 5 | * Copyright (c) 2016 "Cowboy" Ben Alman, contributors 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | exports.init = function(grunt) { 12 | 13 | // Nodejs libs. 14 | var path = require('path'); 15 | 16 | // External libs. 17 | var semver = require('semver'); 18 | var Tempfile = require('temporary').File; 19 | var EventEmitter2 = require('eventemitter2').EventEmitter2; 20 | var rimraf = require('rimraf'); 21 | 22 | // Get path to phantomjs binary 23 | var binPath = require('phantomjs-prebuilt').path; 24 | 25 | // The module to be exported is an event emitter. 26 | var exports = new EventEmitter2({wildcard: true, maxListeners: 0}); 27 | 28 | // Get an asset file, local to the root of the project. 29 | var asset = path.join.bind(null, __dirname, '..'); 30 | 31 | // Call this when everything has finished successfully... or when something 32 | // horrible happens, and you need to clean up and abort. 33 | var halted; 34 | exports.halt = function() { 35 | halted = true; 36 | }; 37 | 38 | // Start PhantomJS process. 39 | exports.spawn = function(pageUrl, options) { 40 | // Create temporary file to be used for grunt-phantom communication. 41 | var tempfile = new Tempfile(); 42 | // Timeout ID. 43 | var id; 44 | // The number of tempfile lines already read. 45 | var n = 0; 46 | // Reset halted flag. 47 | halted = null; 48 | // Handle for spawned process. 49 | var phantomJSHandle; 50 | // Default options. 51 | if (typeof options.killTimeout !== 'number') { options.killTimeout = 1000; } 52 | options.options = options.options || {}; 53 | 54 | // All done? Clean up! 55 | var cleanup = function(done, immediate) { 56 | clearTimeout(id); 57 | var kill = function() { 58 | // Only kill process if it has a pid, otherwise an error would be thrown. 59 | if (phantomJSHandle.pid) { 60 | phantomJSHandle.kill(); 61 | } 62 | rimraf(tempfile.path, function(err) { 63 | if (err) { throw err; } 64 | }); 65 | if (typeof done === 'function') { done(null); } 66 | }; 67 | // Allow immediate killing in an error condition. 68 | if (immediate) { return kill(); } 69 | // Wait until the timeout expires to kill the process, so it can clean up. 70 | setTimeout(kill, options.killTimeout); 71 | }; 72 | 73 | // Internal methods. 74 | var privates = { 75 | // Abort if PhantomJS version isn't adequate. 76 | version: function(version) { 77 | var current = [version.major, version.minor, version.patch].join('.'); 78 | var required = '>= 1.6.0'; 79 | if (!semver.satisfies(current, required)) { 80 | exports.halt(); 81 | grunt.log.writeln(); 82 | grunt.log.errorlns( 83 | 'In order for this task to work properly, PhantomJS version ' + 84 | required + ' must be installed, but version ' + current + 85 | ' was detected.' 86 | ); 87 | grunt.warn('The correct version of PhantomJS needs to be installed.', 127); 88 | } 89 | } 90 | }; 91 | 92 | // It's simple. As the page running in PhantomJS alerts messages, they 93 | // are written as JSON to a temporary file. This polling loop checks that 94 | // file for new lines, and for each one parses its JSON and emits the 95 | // corresponding event with the specified arguments. 96 | (function loopy() { 97 | // Disable logging temporarily. 98 | grunt.log.muted = true; 99 | // Read the file, splitting lines on \n, and removing a trailing line. 100 | var lines = grunt.file.read(tempfile.path).split('\n').slice(0, -1); 101 | // Re-enable logging. 102 | grunt.log.muted = false; 103 | // Iterate over all lines that haven't already been processed. 104 | var done = lines.slice(n).some(function(line) { 105 | // Get args and method. 106 | var args = JSON.parse(line); 107 | var eventName = args[0]; 108 | // Debugging messages. 109 | grunt.log.debug(JSON.stringify(['phantomjs'].concat(args)).magenta); 110 | if (eventName === 'private') { 111 | // If a private (internal) message is passed, execute the 112 | // corresponding method. 113 | privates[args[1]].apply(null, args.slice(2)); 114 | } else { 115 | // Otherwise, emit the event with its arguments. 116 | exports.emit.apply(exports, args); 117 | } 118 | // If halted, return true. Because the Array#some method was used, 119 | // this not only sets "done" to true, but stops further iteration 120 | // from occurring. 121 | return halted; 122 | }); 123 | 124 | if (done) { 125 | // All done. 126 | cleanup(options.done); 127 | } else { 128 | // Update n so previously processed lines are ignored. 129 | n = lines.length; 130 | // Check back in a little bit. 131 | id = setTimeout(loopy, 100); 132 | } 133 | }()); 134 | 135 | // Process options. 136 | var failCode = options.failCode || 0; 137 | 138 | // An array of optional PhantomJS --args. 139 | var args = []; 140 | // Additional options for the PhantomJS main.js script. 141 | var opts = {}; 142 | 143 | // Build args array / opts object. 144 | Object.keys(options.options).forEach(function(key) { 145 | if (/^\-\-/.test(key)) { 146 | args.push(key + '=' + options.options[key]); 147 | } else { 148 | opts[key] = options.options[key]; 149 | } 150 | }); 151 | 152 | // Keep -- PhantomJS args first, followed by grunt-specific args. 153 | args.push( 154 | // The main PhantomJS script file. 155 | opts.phantomScript || asset('phantomjs/main.js'), 156 | // The temporary file used for communications. 157 | tempfile.path, 158 | // URL or path to the page .html test file to run. 159 | pageUrl, 160 | // Additional PhantomJS options. 161 | JSON.stringify(opts) 162 | ); 163 | 164 | grunt.log.debug(JSON.stringify(args)); 165 | 166 | // Actually spawn PhantomJS. 167 | return phantomJSHandle = grunt.util.spawn({ 168 | cmd: binPath, 169 | args: args 170 | }, function(err, result, code) { 171 | if (!err) { return; } 172 | 173 | // Ignore intentional cleanup. 174 | if (code === 15 || code === null /* SIGTERM */) { return; } 175 | 176 | // If we're here, something went horribly wrong. 177 | cleanup(null, true /* immediate */); 178 | grunt.verbose.or.writeln(); 179 | grunt.log.write('PhantomJS threw an error:').error(); 180 | // Print result to stderr because sometimes the 127 code means that a shared library is missing 181 | String(result).split('\n').forEach(grunt.log.error, grunt.log); 182 | if (code === 127) { 183 | grunt.log.errorlns( 184 | 'In order for this task to work properly, PhantomJS must be installed locally via NPM. ' + 185 | 'If you\'re seeing this message, generally that means the NPM install has failed. ' + 186 | 'Please submit an issue providing as much detail as possible at: ' + 187 | 'https://github.com/gruntjs/grunt-lib-phantomjs/issues' 188 | ); 189 | grunt.warn('PhantomJS not found.', failCode); 190 | } else { 191 | grunt.warn('PhantomJS exited unexpectedly with exit code ' + code + '.', failCode); 192 | } 193 | options.done(code); 194 | }); 195 | }; 196 | 197 | return exports; 198 | }; 199 | --------------------------------------------------------------------------------