├── .npmignore ├── .travis.yml ├── LICENCE ├── index.js ├── lib └── promise.js ├── package.json ├── readme.md └── test ├── files ├── alert.html ├── content.html ├── index.html ├── inject.js ├── jserr.html └── page1.html └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "4" 6 | - "iojs" -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Pete Cooper 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Promise = require('./lib/promise'); 2 | var phantom = require('phantom'); 3 | var util = require('util'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var defaults = require('defaults'); 6 | var changeCase = require('change-case'); 7 | 8 | var OPTIONS = { 9 | params: [ 10 | 'diskCache', 11 | 'ignoreSslErrors', 12 | 'localStoragePath', 13 | 'localToRemoteUrlAccess', 14 | 'maxDiskCacheSize', 15 | 'proxy', 16 | 'proxyType', 17 | 'proxyAuth', 18 | 'sslCertificatesPath', 19 | 'webSecurity' 20 | ], 21 | options: [ 22 | 'path', 23 | 'binary', 24 | 'port' 25 | ], 26 | page: [ 27 | 'javascriptEnabled', 28 | 'loadImages', 29 | 'localToRemoteUrlAccessEnabled', 30 | 'userAgent', 31 | 'userName', 32 | 'password', 33 | 'XSSAuditingEnabled', 34 | 'webSecurityEnabled', 35 | 'resourceTimeout' 36 | ], 37 | extras: [ 38 | 'timeout' 39 | ] 40 | }; 41 | 42 | var DEFAULTS = { 43 | diskCache: false, 44 | ignoreSslErrors: true, 45 | loadImages: true, 46 | localStoragePath: null, 47 | localToRemoteUrlAccess: false, 48 | maxDiskCacheSize: null, 49 | path: null, 50 | binary: null, 51 | port: null, 52 | proxy: null, 53 | proxyType: 'http', 54 | proxyAuth: null, 55 | sslCertificatesPath: null, 56 | timeout: 5000, 57 | webSecurity: true, 58 | javascriptEnabled: null, 59 | loadImages: null, 60 | localToRemoteUrlAccessEnabled: null, 61 | userAgent: null, 62 | userName: null, 63 | password: null, 64 | XSSAuditingEnabled: null, 65 | webSecurityEnabled: null, 66 | resourceTimeout: null, 67 | }; 68 | 69 | module.exports = Phantasma = function (options) { 70 | EventEmitter.call(this); 71 | this.ph = null; 72 | this.page = null; 73 | this.promise = Promise(this); 74 | this.options = defaults(options, DEFAULTS); 75 | return this.init(); 76 | }; 77 | 78 | util.inherits(Phantasma, EventEmitter); 79 | 80 | Phantasma.prototype.init = function () { 81 | var self = this; 82 | 83 | var options = {parameters: {},dnodeOpts: {weak: false}}; 84 | var pageOptions = {}; 85 | for(var o in this.options){ 86 | if(this.options[o] !== null){ 87 | if(OPTIONS.params.indexOf(o) !== -1){ 88 | options.parameters[changeCase.paramCase(o)] = this.options[o]; 89 | } 90 | if(OPTIONS.options.indexOf(o) !== -1){ 91 | options[o] = this.options[o]; 92 | } 93 | if(OPTIONS.page.indexOf(o) !== -1){ 94 | pageOptions[o] = this.options[o]; 95 | } 96 | } 97 | } 98 | 99 | return new this.promise(function (resolve, reject) { 100 | phantom.create(function (ph) { 101 | self.ph = ph; 102 | ph.createPage(function (page) { 103 | self.page = page; 104 | // map phantom callback to signals 105 | page.set('onAlert', function (msg) { 106 | self.emit('onAlert', msg); 107 | }); 108 | page.set('onConsoleMessage', function (msg, lineNum, sourceId) { 109 | self.emit('onConsoleMessage', msg, lineNum, sourceId); 110 | }); 111 | page.set('onError', function (msg, trace) { 112 | self.emit('onError', msg, trace); 113 | }); 114 | page.set('onLoadFinished', function (status) { 115 | self.emit('onLoadFinished', status); 116 | }); 117 | page.set('onLoadStarted', function () { 118 | self.emit('onLoadStarted'); 119 | }); 120 | page.set('onNavigationRequested', function (url, type, willNavigate, main) { 121 | self.emit('onNavigationRequested', url, type, willNavigate, main); 122 | }); 123 | page.set('onResourceReceived', function (response) { 124 | self.emit('onResourceReceived', response); 125 | }); 126 | page.set('onResourceRequested', function (requestData, networkRequest) { 127 | self.emit('onResourceRequested', requestData, networkRequest); 128 | }); 129 | page.set('onResourceTimeout', function (request) { 130 | self.emit('onResourceTimeout', request); 131 | }); 132 | page.set('onUrlChanged', function (url) { 133 | self.emit('onUrlChanged', url); 134 | }); 135 | 136 | if(Object.keys(pageOptions).length){ 137 | var settings = []; 138 | for(var o in pageOptions){ 139 | settings.push(self.pageSetting(o, pageOptions[o])); 140 | } 141 | self.promise.all(settings).then(function () { 142 | resolve(); 143 | }); 144 | }else{ 145 | resolve(); 146 | } 147 | 148 | }); 149 | }, options); 150 | }); 151 | 152 | }; 153 | 154 | Phantasma.prototype.getPid = function () { 155 | return this.ph ? this.ph.process.pid : null; 156 | }; 157 | 158 | Phantasma.prototype.pageSetting = function (setting, value) { 159 | var self = this; 160 | 161 | return new this.promise(function (resolve, reject) { 162 | self.page.set('settings.' + setting, value, resolve); 163 | }); 164 | }; 165 | 166 | Phantasma.prototype.open = function (url) { 167 | var self = this; 168 | 169 | return new this.promise(function (resolve, reject) { 170 | if(!self.page) return reject('tried to open before page created'); 171 | 172 | self.page.open(url, function (status) { 173 | if (status === 'fail') return reject(status); 174 | resolve(status); 175 | }); 176 | }).timeout(this.options.timeout); 177 | }; 178 | 179 | Phantasma.prototype.exit = function () { 180 | var self = this; 181 | 182 | return new this.promise(function (resolve, reject) { 183 | self.ph.exit(); 184 | resolve(); 185 | }); 186 | }; 187 | 188 | Phantasma.prototype.viewport = function (width, height) { 189 | var self = this; 190 | 191 | return new this.promise(function (resolve, reject) { 192 | if(!self.page) return reject('tried to set viewport before page created'); 193 | self.page.set('viewportSize', {width: width, height: height}, function (result) { 194 | resolve(result); 195 | }); 196 | }); 197 | }; 198 | 199 | Phantasma.prototype.wait = function () { 200 | var self = this; 201 | 202 | return new this.promise(function (resolve, reject) { 203 | self.once('onLoadFinished', function (status) { 204 | resolve(status); 205 | }); 206 | }).timeout(this.options.timeout); 207 | }; 208 | 209 | Phantasma.prototype.screenshot = function (path) { 210 | var self = this; 211 | 212 | return new this.promise(function (resolve, reject) { 213 | self.page.render(path, resolve); 214 | }); 215 | }; 216 | 217 | Phantasma.prototype.evaluate = function (fn) { 218 | var self = this; 219 | 220 | var args = [].slice.call(arguments); 221 | return new this.promise(function (resolve, reject) { 222 | if(!self.page) return reject('tried to evaluate before page created'); 223 | args = [fn, resolve].concat(args.slice(1)); 224 | self.page.evaluate.apply(null, args); 225 | }); 226 | }; 227 | 228 | Phantasma.prototype.type = function (selector, value) { 229 | var self = this; 230 | 231 | return this.focus(selector) 232 | .then(function () { 233 | return self.page.sendEvent('keypress', value); 234 | }).delay(50); 235 | }; 236 | 237 | Phantasma.prototype.value = function (selector, value) { 238 | var self = this; 239 | 240 | return this.evaluate(function (selector, value) { 241 | document.querySelector(selector).value = value; 242 | }, selector, value); 243 | }; 244 | 245 | Phantasma.prototype.select = function (selector, value) { 246 | var self = this; 247 | 248 | return this.evaluate(function (selector, value) { 249 | var element = document.querySelector(selector); 250 | var evt = document.createEvent('HTMLEvents'); 251 | element.value = value; 252 | evt.initEvent('change', true, true); 253 | element.dispatchEvent(evt); 254 | }, selector, value); 255 | }; 256 | 257 | Phantasma.prototype.click = function (selector, y) { 258 | var self = this; 259 | 260 | if (y) { 261 | return new this.promise(function (resolve, reject) { 262 | self.page.sendEvent('click', selector, y); 263 | resolve(); 264 | }); 265 | } 266 | 267 | return this.evaluate(function (selector) { 268 | var evt = document.createEvent('MouseEvent'); 269 | evt.initEvent('click', true, true); 270 | var ele = document.querySelector(selector); 271 | ele.dispatchEvent(evt); 272 | }, selector); 273 | }; 274 | 275 | Phantasma.prototype.title = function () { 276 | return this.evaluate(function () { 277 | return document.title; 278 | }); 279 | }; 280 | 281 | Phantasma.prototype.url = function () { 282 | return this.evaluate(function () { 283 | return document.location.href; 284 | }); 285 | }; 286 | 287 | Phantasma.prototype.forward = function () { 288 | var self = this; 289 | 290 | return new this.promise(function (resolve, reject) { 291 | self.page.goForward(); 292 | resolve(); 293 | }).wait(); 294 | }; 295 | 296 | Phantasma.prototype.back = function () { 297 | var self = this; 298 | 299 | return new this.promise(function (resolve, reject) { 300 | self.page.goBack(); 301 | resolve(); 302 | }).wait(); 303 | }; 304 | 305 | Phantasma.prototype.refresh = function () { 306 | var self = this; 307 | 308 | return new this.promise(function (resolve, reject) { 309 | self.page.reload(); 310 | resolve(); 311 | }).wait(); 312 | }; 313 | 314 | Phantasma.prototype.focus = function (selector) { 315 | var self = this; 316 | 317 | return this.evaluate(function (selector) { 318 | document.querySelector(selector).focus(); 319 | }, selector); 320 | }; 321 | 322 | Phantasma.prototype.injectJs = function (path) { 323 | var self = this; 324 | 325 | return new this.promise(function (resolve, reject) { 326 | self.page.injectJs(path, function (status) { 327 | resolve(status); 328 | }); 329 | }); 330 | }; 331 | 332 | Phantasma.prototype.injectCss = function (style) { 333 | var self = this; 334 | 335 | return this.evaluate(function (style) { 336 | var ele = document.createElement('style'); 337 | ele.innerHTML = style; 338 | document.head.appendChild(ele); 339 | }, style); 340 | }; 341 | 342 | Phantasma.prototype.content = function (html) { 343 | var self = this; 344 | 345 | return new this.promise(function (resolve, reject) { 346 | if(html){ 347 | self.page.setContent(html, null, resolve); 348 | }else{ 349 | self.page.getContent(function (content) { 350 | resolve(content); 351 | }); 352 | } 353 | }); 354 | }; 355 | Phantasma.prototype.screenshotDomElement = function (selector, path) { 356 | var self = this; 357 | return this.evaluate(function (selector) { 358 | return document.querySelector(selector).getBoundingClientRect(); 359 | }, selector).then(function (value) { 360 | self.page.set('clipRect', value); 361 | return new self.promise(function (resolve, reject) { 362 | self.page.render(path, resolve); 363 | self.restoreRect(); 364 | }) 365 | }) 366 | } 367 | 368 | Phantasma.prototype.restoreRect = function () { 369 | var self = this; 370 | return this.evaluate(function () { 371 | return document.querySelector('body').getBoundingClientRect(); 372 | }).then(function (value) { 373 | self.page.set('clipRect', value); 374 | }) 375 | } 376 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | var promise = require("bluebird/js/main/promise"); 2 | 3 | module.exports = function (pa) { 4 | 5 | var Promise = promise(); 6 | var phantasma = pa; 7 | 8 | // methods to run after previous promise has resolved 9 | [ 10 | 'open', 11 | 'exit', 12 | 'wait', 13 | 'viewport', 14 | 'screenshot', 15 | 'evaluate', 16 | 'type', 17 | 'value', 18 | 'select', 19 | 'click', 20 | 'title', 21 | 'url', 22 | 'forward', 23 | 'back', 24 | 'refresh', 25 | 'focus', 26 | 'injectJs', 27 | 'injectCss', 28 | 'content', 29 | 'pageSetting', 30 | 'screenshotDomElement' 31 | ].forEach(function (method) { 32 | Promise.prototype[method] = function () { 33 | var self = this; 34 | var args = [].slice.call(arguments); 35 | return this.then(function () { 36 | return phantasma[method].apply(phantasma, args); 37 | }); 38 | }; 39 | }); 40 | 41 | // methods to run immediately 42 | ['on', 'once', 'getPid', 'removeListener'].forEach(function (method) { 43 | Promise.prototype[method] = function () { 44 | var args = [].slice.call(arguments); 45 | return phantasma[method].apply(phantasma, args); 46 | }; 47 | }); 48 | 49 | return Promise; 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantasma", 3 | "version": "1.4.0", 4 | "description": "A high level promise based wrapper for phantomjs", 5 | "keywords": [ 6 | "phantom", 7 | "phantomjs" 8 | ], 9 | "main": "index.js", 10 | "homepage": "https://github.com/petecoop/phantasma", 11 | "bugs": "https://github.com/petecoop/phantasma/issues", 12 | "author": "petecoop (http://petecoop.co.uk/)", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/petecoop/phantasma.git" 16 | }, 17 | "scripts": { 18 | "test": "mocha --slow=1000 --timeout=5000" 19 | }, 20 | "license": "ISC", 21 | "dependencies": { 22 | "bluebird": "^2.9.13", 23 | "change-case": "^2.2.0", 24 | "defaults": "^1.0.0", 25 | "phantom": "^0.7.2" 26 | }, 27 | "devDependencies": { 28 | "finalhandler": "^0.3.3", 29 | "mocha": "^2.2.1", 30 | "rimraf": "^2.3.2", 31 | "serve-static": "^1.9.1", 32 | "should": "^5.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **No longer maintained** 2 | 3 | I'd really recommend using [Puppeteer](https://github.com/GoogleChrome/puppeteer), which solves the problem I was trying to solve here and is also really nice and quick. 4 | 5 | # Phantasma 6 | [![Build Status](https://img.shields.io/travis/petecoop/phantasma.svg)](https://travis-ci.org/petecoop/phantasma) 7 | [![NPM Version](https://img.shields.io/npm/v/phantasma.svg)](https://www.npmjs.org/package/phantasma) 8 | [![NPM Downloads](https://img.shields.io/npm/dm/phantasma.svg)](https://www.npmjs.org/package/phantasma) 9 | 10 | 11 | A high level promise based wrapper for [PhantomJS](http://phantomjs.org/) 12 | 13 | The aim is to make interacting with PhantomJS from node as simple as possible. All actions are asynchronous and return a [bluebird](https://www.npmjs.org/package/bluebird) promise. The promises have been extended with Phantasma methods, allowing for a fluent API. 14 | 15 | This project is heavily influenced by [Nightmare](https://github.com/segmentio/nightmare), but different - Nightmare queues up actions which are then exectued when `.run()` is called, once this is done phantomjs is exited. This is fine if you already know the actions you want to take however it's not possible to change the flow of actions mid-way e.g. if sometimes a popup/button appears on a page that you want to click before continuing with the next action. Phantasma takes a different approach - using promises which leaves queueing up to the promise library (bluebird) and leaves you in control of when to exit the phantomjs process. 16 | 17 | ## Install 18 | 19 | - Install PhantomJs: http://phantomjs.org/download.html 20 | 21 | - `npm install phantasma` 22 | 23 | ## Examples 24 | 25 | ```js 26 | var Phantasma = require('phantasma'); 27 | 28 | var ph = new Phantasma(); 29 | 30 | ph.open('https://duckduckgo.com') 31 | .type('#search_form_input_homepage', 'phantomjs') 32 | .click('#search_button_homepage') 33 | .wait() 34 | .screenshot('screenshot.png') 35 | .evaluate(function () { 36 | return document.querySelectorAll('.result').length; 37 | }) 38 | .then(function (num) { 39 | console.log(num + ' results'); 40 | }) 41 | .catch(function (e) { 42 | console.log('error', e); 43 | }) 44 | .finally(function () { 45 | console.log('done!'); 46 | ph.exit(); 47 | }); 48 | ``` 49 | 50 | Any of the above methods can be replaced with a `.then` e.g. 51 | 52 | ```js 53 | var Phantasma = require('phantasma'); 54 | 55 | var ph = new Phantasma(); 56 | 57 | ph.then(function () { 58 | return ph.open('https://duckduckgo.com'); 59 | }) 60 | .screenshot('screenshot.png') 61 | .finally(function () { 62 | ph.exit(); 63 | }); 64 | 65 | ``` 66 | 67 | This allows for conditionally changing the flow depending on the result of the last request: 68 | 69 | ```js 70 | var ph = new Phantasma(); 71 | 72 | ph.open('https://duckduckgo.com') 73 | .type('#search_form_input_homepage', 'akjsdhjashda') 74 | .click('#search_button_homepage') 75 | .wait() 76 | .evaluate(function () { 77 | return document.querySelectorAll('.result').length; 78 | }) 79 | .then(function (num) { 80 | if(!num){ 81 | return ph.type('#search_form_input', 'phantomjs') 82 | .click('#search_button') 83 | .wait() 84 | .screenshot('screenshot.png'); 85 | } 86 | return ph.screenshot('screenshot.png'); 87 | }) 88 | .finally(function () { 89 | ph.exit(); 90 | }); 91 | ``` 92 | 93 | 94 | ## API 95 | 96 | #### new Phantasma(options) 97 | Create a new instance, initiates the phantomjs instance 98 | 99 | The available options are: 100 | 101 | - `diskCache: [true|false]`: enables disk cache (default is `false`). 102 | - `ignoreSslErrors: [true|false]`: ignores SSL errors, such as expired or self-signed certificate errors (default is `true`). 103 | - `loadImages: [true|false]`: load all inlined images (default is `true`). 104 | - `localStoragePath: '/some/path'`: path to save LocalStorage content and WebSQL content (no default). 105 | - `localStorageQuota: [Number]`: maximum size to allow for data (no default). 106 | - `localToRemoteUrlAccess: [true|false]`: allows local content to access remote URL (default is `false`). 107 | - `maxDiskCacheSize: [Number]`: limits the size of disk cache in KB (no default). 108 | - `binary`: specify a different custom path to PhantomJS (no default). 109 | - `port: [Number]`: specifies the phantomjs port. 110 | - `proxy: 'address:port'`: specifies the proxy server to use (e.g. `proxy: '192.168.1.42:8080'`) (no default). 111 | - `proxyType: [http|socks5|none]`: specifies the type of the proxy server (default is `http`) (no default). 112 | - `proxyAuth`: specifies the authentication information for the proxy, e.g. `proxyAuth: 'username:password'`) (no default). 113 | - `sslProtocol: [sslv3|sslv2|tlsv1|any]` sets the SSL protocol for secure connections (default is `any`). 114 | - `sslCertificatesPath: '/some/path'` Sets the location for custom CA certificates (if none set, uses system `default`). 115 | - `timeout [Number]`: how long to wait for page loads in ms (default is `5000`). 116 | - `webSecurity: [true|false]`: enables web security and forbids cross-domain XHR (default is `true`). 117 | 118 | #### Page Settings 119 | These options can be passed into `new Phantasma(options)`, alternatively they can be set individually afterwards using the `.pageSetting(setting, value)` method. 120 | 121 | - `javascriptEnabled: [true|false]`: defines whether to execute the script in the page or not (defaults to `true`). 122 | - `loadImages: [true|false]`: defines whether to load the inlined images or not (defaults to `true`). 123 | - `localToRemoteUrlAccessEnabled: [true|false]`: defines whether local resource (e.g. from file) can access remote URLs or not (defaults to `false`). 124 | - `userAgent: String`: defines the user agent sent to server when the web page requests resources. 125 | - `userName: String`: sets the user name used for HTTP authentication. 126 | - `password: String`: sets the password used for HTTP authentication. 127 | - `XSSAuditingEnabled: [true|false]`: defines whether load requests should be monitored for cross-site scripting attempts (defaults to `false`). 128 | - `webSecurityEnabled: [true|false]`: defines whether web security should be enabled or not (defaults to `true`). 129 | - `resourceTimeout: Number`: (in milli-secs) defines the timeout after which any resource requested will stop trying and proceed with other parts of the page. `onResourceTimeout` event will be called on timeout. 130 | 131 | ### Methods 132 | 133 | #### .open(url) 134 | Load the page at `url`. Will throw a Timeout error if it takes longer to complete than the timeout setting. 135 | 136 | #### .wait() 137 | Wait until a page finishes loading, typically after a `.click()`. Will throw a Timeout error if it takes longer to complete than the timeout setting. 138 | 139 | #### .exit() 140 | Close the phantomjs process. 141 | 142 | #### .click(selector) 143 | Clicks the `selector` element. 144 | 145 | #### .click(x, y) 146 | Clicks at the position given. 147 | 148 | #### .type(selector, text) 149 | Enters the `text` provided into the `selector` element. 150 | 151 | #### .value(selector, text) 152 | Sets the `text` provided as the value of the `selector` element. 153 | 154 | #### .select(selector, value) 155 | Sets the `value` of a select element to `value`. 156 | 157 | #### .evaluate(fn, arg1, arg2,...) 158 | Invokes `fn` on the page with `arg1, arg2,...`. All the `args` are optional. On completion it passes the return value of `fn` to the resolved promise. Example: 159 | 160 | ```js 161 | var Phantasma = require('phantasma'); 162 | var p1 = 1; 163 | var p2 = 2; 164 | 165 | var ph = new Phantasma(); 166 | 167 | ph.evaluate(function (param1, param2) { 168 | // now we're executing inside the browser scope. 169 | return param1 + param2; 170 | }, p1, p2) 171 | .then(function (result) { 172 | // now we're inside Node scope again 173 | console.log(result); 174 | }) 175 | .finally(function () { 176 | ph.exit(); 177 | }); 178 | ``` 179 | 180 | #### .viewport(width, height) 181 | Set the viewport dimensions 182 | 183 | #### .screenshot(path) 184 | Saves a screenshot of the current page to the specified `path`. Useful for debugging. Note the path must include the file extension. Supported formats include .png, .gif, .jpeg, and .pdf. 185 | 186 | #### .screenshotDomElement(selector,path) 187 | Saves an screenshot of an specific DOM element as image to the specified `path`.Note the path must include the file extension. Supported formats include .png, .gif, .jpeg, and .pdf. 188 | 189 | #### .title() 190 | Get the title of the current page, the result is passed to the resolved promise. 191 | 192 | #### .url() 193 | Get the url of the current page, the result is passed to the resolved promise. 194 | 195 | #### .back() 196 | Go back to the previous page. This will `.wait()` untill the page has loaded. 197 | 198 | #### .forward() 199 | Go forward to the next page. This will `.wait()` untill the page has loaded. 200 | 201 | #### .refresh() 202 | refresh the current page. This will `.wait()` untill the page has loaded. 203 | 204 | #### .focus(selector) 205 | Focus the `selector` element. 206 | 207 | #### .injectJs(path) 208 | Inject javascript at `path` into the currently open page. 209 | 210 | #### .injectCss(style) 211 | Inject CSS string `style` into the currently open page. 212 | 213 | #### .content(html) 214 | Get or set the content of the page, if `html` is set it will set, if not it will get. 215 | 216 | #### .pageSetting(setting, value) 217 | Set a page setting. 218 | 219 | ### Events 220 | 221 | Events extends node's EventEmitter. 222 | 223 | #### .on(event, callback) 224 | Executes `callback` when the `event` is emitted. 225 | 226 | Example: 227 | 228 | ```js 229 | var Phantasma = require('phantasma'); 230 | 231 | var ph = new Phantasma(); 232 | 233 | ph.open('https://duckduckgo.com') 234 | .type('#search_form_input_homepage', 'phantomjs') 235 | .click('#search_button_homepage') 236 | .wait() 237 | .catch(function (e) { 238 | console.log('error', e); 239 | }) 240 | .finally(function () { 241 | console.log('done!'); 242 | ph.exit(); 243 | }).on('onUrlChanged', function (url) { 244 | console.log('url change', url); 245 | }); 246 | ``` 247 | 248 | #### .once(event, callback) 249 | Executes `callback` when the `event` is emitted only once. 250 | 251 | #### .removeListener(event, callback) 252 | Removes `callback` from `event` listener. 253 | 254 | #### Supported Events: 255 | 256 | Supports the following phantomjs events, you can read more on these here ([PhantomJS callbacks](https://github.com/ariya/phantomjs/wiki/API-Reference-WebPage#callbacks-list)): 257 | 258 | - `onAlert` - callback(msg) 259 | - `onConsoleMessage` - callback(msg, lineNum, sourceId) 260 | - `onError` - callback(msg, trace) 261 | - `onLoadFinished` - callback(status) 262 | - `onLoadStarted` - callback() 263 | - `onNavigationRequested` - callback(url, type, willNavigate, main) 264 | - `onResourceReceived` - callback(response) 265 | - `onResourceRequested` - callback(requestData, networkRequest) 266 | - `onResourceTimeout` - callback(request) 267 | - `onUrlChanged` - callback(url) 268 | 269 | ## Promise methods 270 | 271 | You can use any of the methods available to bluebird [found here](https://github.com/petkaantonov/bluebird/blob/master/API.md). 272 | 273 | The most useful methods are: 274 | 275 | #### .then(fulfillHandler, rejectHandler) 276 | Returns a new promise chained from the previous promise. The return value of the previous promise will be passed into this promise. 277 | 278 | 279 | #### .finally(Function handler) 280 | Pass a handler that will be ran regardless of the outcome of the previous promises. Useful for cleaning up the Phantasma process e.g. 281 | 282 | ```js 283 | .finally(function () { 284 | ph.exit(); 285 | }); 286 | ``` 287 | 288 | #### .catch(Function handler) 289 | This is a catch-all exception handler - it can be used to find and log an error. e.g. 290 | 291 | ```js 292 | .catch(function (e) { 293 | console.log(e); 294 | }); 295 | ``` 296 | 297 | #### .delay(ms) 298 | Delay the next promise for `ms` milliseconds 299 | 300 | ## License 301 | 302 | [ISC](http://en.wikipedia.org/wiki/ISC_license) 303 | 304 | Copyright (c) 2014, Pete Cooper - pete@petecoop.co.uk 305 | 306 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 307 | 308 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 309 | -------------------------------------------------------------------------------- /test/files/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alert 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /test/files/content.html: -------------------------------------------------------------------------------- 1 |

Test

-------------------------------------------------------------------------------- /test/files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test 6 | 7 | 8 |

Test Page

9 | 12 | 13 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /test/files/inject.js: -------------------------------------------------------------------------------- 1 | var test = 'testing!'; -------------------------------------------------------------------------------- /test/files/jserr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS Error 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /test/files/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page1 6 | 7 | 8 |

Page1 test page

9 | 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var Phantasma = require('../index'); 2 | var should = require('should'); 3 | var fs = require('fs'); 4 | var rimraf = require('rimraf'); 5 | var http = require('http'); 6 | var serveStatic = require('serve-static'); 7 | var finalhandler = require('finalhandler'); 8 | 9 | describe('Phantasma', function () { 10 | this.timeout(10000); 11 | 12 | var server, ph; 13 | 14 | before(function (done) { 15 | 16 | var serve = serveStatic('./test/files'); 17 | server = http.createServer(function(req, res){ 18 | var d = finalhandler(req, res); 19 | serve(req, res, d); 20 | }); 21 | server.listen(3000, function () { 22 | done(); 23 | }); 24 | }); 25 | 26 | after(function (done) { 27 | ph.exit(); 28 | server.close(function () { 29 | rimraf('test/temp', done); 30 | }); 31 | }); 32 | 33 | beforeEach(function () { 34 | ph = new Phantasma({timeout: 10000}); 35 | }); 36 | 37 | afterEach(function () { 38 | ph.exit(); 39 | }); 40 | 41 | it('should be constructable', function () { 42 | ph.should.be.ok; 43 | }); 44 | 45 | describe('Methods', function () { 46 | 47 | it('should open page', function () { 48 | return ph.open('http://localhost:3000') 49 | .then(function (status) { 50 | status.should.equal('success'); 51 | }); 52 | }); 53 | 54 | it('should reject when open fails', function () { 55 | return ph.open('http://localhost:3001') 56 | .then(function (status) { 57 | throw new Error('failed request did not reject'); 58 | }) 59 | .catch(function (status) { 60 | status.should.equal('fail'); 61 | }); 62 | }); 63 | 64 | it('should get the page title', function () { 65 | return ph.open('http://localhost:3000') 66 | .title() 67 | .then(function (title) { 68 | title.should.equal('Test'); 69 | }); 70 | }); 71 | 72 | it('should follow a link', function () { 73 | return ph.open('http://localhost:3000') 74 | .click('#link') 75 | .wait() 76 | .url() 77 | .then(function (url) { 78 | url.should.equal('http://localhost:3000/page1.html'); 79 | }); 80 | }); 81 | 82 | it('should enter text', function () { 83 | return ph.open('http://localhost:3000') 84 | .type('#typehere', 'test value') 85 | .evaluate(function () { 86 | return document.querySelector('#typehere').value; 87 | }) 88 | .then(function (val) { 89 | val.should.equal('test value'); 90 | }); 91 | }); 92 | 93 | it('should set value', function () { 94 | return ph.open('http://localhost:3000') 95 | .value('#typehere', 'test value') 96 | .evaluate(function () { 97 | return document.querySelector('#typehere').value; 98 | }) 99 | .then(function (val) { 100 | val.should.equal('test value'); 101 | }); 102 | }); 103 | 104 | it('should select a value', function () { 105 | return ph.open('http://localhost:3000') 106 | .select('#selectthis', '2') 107 | .evaluate(function () { 108 | return document.querySelector('#selectthis').value; 109 | }) 110 | .then(function (val) { 111 | val.should.equal('2'); 112 | }); 113 | }); 114 | 115 | it('should take screenshots', function () { 116 | var path = 'test/temp/screenshot.png'; 117 | return ph.open('http://localhost:3000') 118 | .screenshot(path) 119 | .then(function () { 120 | fs.existsSync(path).should.be.true; 121 | }); 122 | }); 123 | 124 | it('should screenshot h1 and save it as image', function () { 125 | var path = 'test/temp/h1.png'; 126 | return ph.open('http://localhost:3000') 127 | .screenshotDomElement('h1[id="heading"]',path) 128 | .then(function () { 129 | fs.existsSync(path).should.be.true; 130 | }); 131 | }); 132 | 133 | it('should navigate backwards and forwards', function () { 134 | return ph.open('http://localhost:3000') 135 | .open('http://localhost:3000/page1.html') 136 | .back() 137 | .url() 138 | .then(function (url) { 139 | url.should.equal('http://localhost:3000/'); 140 | }) 141 | .forward() 142 | .url(function (url) { 143 | url.should.equal('http://localhost:3000/page1.html'); 144 | }); 145 | }); 146 | 147 | it('should refresh the page', function () { 148 | var count = 0; 149 | ph.on('onLoadFinished', function () { 150 | count++; 151 | }); 152 | 153 | return ph.open('http://localhost:3000') 154 | .refresh() 155 | .then(function () { 156 | count.should.equal(2); 157 | }); 158 | }); 159 | 160 | it('should focus an element', function () { 161 | return ph.open('http://localhost:3000') 162 | .focus('#typehere') 163 | .evaluate(function () { 164 | return document.activeElement.id; 165 | }) 166 | .then(function (active) { 167 | active.should.equal('typehere'); 168 | }); 169 | }); 170 | 171 | it('should inject javascript', function () { 172 | return ph.open('http://localhost:3000') 173 | .injectJs('test/files/inject.js') 174 | .evaluate(function () { 175 | return test; 176 | }) 177 | .then(function (result) { 178 | result.should.equal('testing!'); 179 | }); 180 | }); 181 | 182 | it('should inject css', function () { 183 | return ph.open('http://localhost:3000') 184 | .injectCss('h1 { color: #ff0000; }') 185 | .evaluate(function () { 186 | var el = document.querySelector('#heading'); 187 | var style = getComputedStyle(el); 188 | return style.color; 189 | }) 190 | .then(function (result) { 191 | result.should.equal('rgb(255, 0, 0)'); 192 | }); 193 | }); 194 | 195 | it('should get content', function () { 196 | return ph.open('http://localhost:3000/content.html') 197 | .content() 198 | .then(function (result) { 199 | result.should.equal('

Test

'); 200 | }); 201 | }); 202 | 203 | it('should set content', function () { 204 | return ph.content('

Test

') 205 | .content() 206 | .then(function (result) { 207 | result.should.equal('

Test

'); 208 | }); 209 | }); 210 | 211 | }); 212 | 213 | describe('Events', function () { 214 | 215 | it('should emit on url change', function (done) { 216 | ph.open('http://localhost:3000') 217 | .once('onUrlChanged', function (url) { 218 | url.should.equal('http://localhost:3000/'); 219 | done(); 220 | }); 221 | }); 222 | 223 | it('should emit on resource requested', function (done) { 224 | ph.open('http://localhost:3000') 225 | .once('onResourceRequested', function (requestData, networkRequest) { 226 | requestData.url.should.equal('http://localhost:3000/'); 227 | requestData.method.should.equal('GET'); 228 | done(); 229 | }); 230 | }); 231 | 232 | it('should emit on resource received', function (done) { 233 | ph.open('http://localhost:3000') 234 | .once('onResourceReceived', function (response) { 235 | response.url.should.equal('http://localhost:3000/'); 236 | done(); 237 | }); 238 | }); 239 | 240 | it('should emit on load started', function (done) { 241 | ph.open('http://localhost:3000') 242 | .once('onLoadStarted', function () { 243 | done(); 244 | }); 245 | }); 246 | 247 | it('should emit on load finished', function (done) { 248 | ph.open('http://localhost:3000') 249 | .once('onLoadFinished', function (status) { 250 | status.should.equal('success'); 251 | done(); 252 | }); 253 | }); 254 | 255 | it('should emit on alert', function (done) { 256 | ph.open('http://localhost:3000/alert.html') 257 | .once('onAlert', function (msg) { 258 | msg.should.equal('test alert message'); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('should emit on javascript error', function (done) { 264 | ph.open('http://localhost:3000/jserr.html') 265 | .once('onError', function (msg) { 266 | msg.should.not.be.empty; 267 | done(); 268 | }); 269 | }); 270 | 271 | it('should remove event listener', function (done) { 272 | var count = 0; 273 | var errorCallback = function (msg){ 274 | count++; 275 | msg.should.not.be.empty; 276 | } 277 | ph.on('onError', errorCallback); 278 | ph.open('http://localhost:3000/jserr.html') 279 | .then(function(){ 280 | ph.removeListener('onError', errorCallback); 281 | }) 282 | .open('http://localhost:3000/jserr.html') 283 | .delay(100) 284 | .then(function(){ 285 | count.should.equal(1); 286 | done(); 287 | }); 288 | }); 289 | 290 | it('should emit on navigation requested', function (done) { 291 | ph.open('http://localhost:3000') 292 | .once('onNavigationRequested', function (url, type, willNavigate, main) { 293 | url.should.equal('http://localhost:3000/'); 294 | willNavigate.should.be.true; 295 | done(); 296 | }); 297 | }); 298 | 299 | }); 300 | 301 | }); 302 | 303 | describe('Phantasma Multiple Processes', function () { 304 | 305 | it('should be able to handle multiple processes', function (done) { 306 | var ph1 = new Phantasma(); 307 | var pid1, pid2; 308 | ph1.then(function () { 309 | pid1 = ph1.getPid(); 310 | 311 | var ph2 = new Phantasma(); 312 | ph2.then(function () { 313 | pid2 = ph2.getPid(); 314 | pid3 = ph1.getPid(); 315 | 316 | pid1.should.equal(pid3); 317 | pid2.should.not.equal(pid3); 318 | done(); 319 | }); 320 | }); 321 | 322 | }); 323 | 324 | }); --------------------------------------------------------------------------------