├── .editorconfig ├── .gitignore ├── .hound.yml ├── .jshintrc ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── Readme.md ├── examples ├── follow.js ├── links.js └── pizza.js ├── files └── jquery-2.1.1.min.js ├── lib ├── HorsemanPromise.js ├── actions.js └── index.js ├── package.json └── test ├── files ├── base.css ├── cookies.txt ├── frame1.html ├── frame2.html ├── frame3.html ├── frames.html ├── index.html ├── jQuery.html ├── newtab.html ├── next.html ├── opennewtab.html ├── plainText.html ├── test.txt ├── testcss.css └── testjs.js └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*.js] 7 | indent_style = tab 8 | 9 | [{package.json,.travis.yml,*.md}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/*.png 3 | test.js -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | fail_on_violations: true 2 | 3 | jshint: 4 | config_file: .jshintrc 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "mocha": true, 5 | "globals" : { 6 | "prompt": false, 7 | "alert": false, 8 | "jQuery": false, 9 | "$": false, 10 | "Promise": true, 11 | "__horseman": true, 12 | "___obj": true 13 | }, 14 | "sub": true, 15 | "boss": true, 16 | "quotmark": "single", 17 | "evil": true, 18 | "maxlen": 80, 19 | "validthis": true, 20 | "strict": true 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | - '5' 7 | - '6' 8 | - '7' 9 | sudo: false 10 | before_install: 11 | sh -c 'npm install phantomjs-prebuilt@$PHANTOMJS || npm install phantomjs@$PHANTOMJS' 12 | env: 13 | - PHANTOMJS=1 14 | - PHANTOMJS=2 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | John Titus 2 | Alex Layton (http://alex.layton.in) 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## 3.3.0 - 2017-04-06 5 | ### Added 6 | - `phantomOptions` option for passing arbitrary PhantomJS CLI options - 7 | thanks @fabiocorneti 8 | - Support for per-action timeouts on `.waitFor` type actions - thanks @piercus 9 | 10 | ### Fixed 11 | - `.boundingRectangle()` sometimes not using jQuery when it could - 12 | thanks @robsgreen 13 | - `.evaluate()` sometimes failing due to an utilized internal variable - 14 | thanks @robertpallas 15 | 16 | ### Changed 17 | - Removed `clientScripts` option (it had no effect anyways) 18 | 19 | ## 3.2.0 - 2016-10-21 20 | ### Added 21 | - `diskCache` and `diskCachePath` options - thanks @efernandesng 22 | 23 | ## 3.1.1 - 2016-06-17 24 | Republish 3.1.0 without unwanted files which broke Windows installation 25 | 26 | ## 3.1.0 - 2016-06-15 27 | ### Added 28 | - .plainText() action - thanks @mzhangARS 29 | 30 | ### Changed 31 | - Removed `port` constructor option (it no longer had any effect) 32 | 33 | ## 3.0.1 - 2016-04-12 34 | ### Fixed 35 | - .injectJs() now properly rejects when it fails 36 | - #158 - bug with `injectBluebird` option 37 | 38 | ## 3.0.0 - 2016-04-06 39 | ### Added 40 | - Support for adding custom actions via `Horseman.registerAction` 41 | 42 | ## 3.0.0-beta2 - 2016-04-05 43 | ### Added 44 | - .closeTab() action removes a tab and closes its page 45 | - tabClosed event fired when a tab is closed 46 | (by .closeTab() or something else) 47 | - .frameName() action 48 | - .frameCount() action 49 | - .frameNames() action 50 | - .switchToFocusedFrame() action 51 | - .switchToFrame() action 52 | - .switchToMainFrame() action 53 | - .switchToParentFrame() action 54 | - .download() action allows getting the raw contents of a URL 55 | (even works for binary files is using PhantomJS 2 or newer) 56 | 57 | ### Changed 58 | - .log() now preserves resolution value of previous action 59 | - More output added to verbose debugging 60 | 61 | ### Fixed 62 | - More readable stack traces from .evaluate() rejections 63 | (especially with long stack traces on) 64 | - Occasional bug with returning undefined in .evaluate() function 65 | 66 | ### Deprecated 67 | - .switchToChildFrame() action is deprecated 68 | since PhantomJS deprecated it 69 | 70 | ## 3.0.0-beta1 - 2016-03-22 71 | ### Added 72 | - bluebird 3 functions are now chainable with Horseman actions 73 | - Support for callback/Promises in .evaluate() functions 74 | - .at() action allows registering callbacks to run in PhantomJS JavaScript 75 | environment (see Reame for details) 76 | - .setProxy() action allows changing Phantom's proxy settings 77 | (requires PhantomJS 2.0.0 or newer) 78 | - .includeJs() action allows injecting JavaScript from a URL 79 | - .put() action 80 | - Support for SlimerJS as well as PhantomJS - thanks @w33ble 81 | - Support for PhantomJS remote debugger - thanks @dustinblackman 82 | - Support for reading cookies.txt files 83 | - Options for injecting bluebird into each page for using Promises in browser 84 | - Option for automatically switching to newly opened tabs 85 | - Option for more verbose debug 86 | 87 | ### Changed 88 | - Horseman Promises now reject on error 89 | - Waiting actions now reject on timeout (timeout callback is still called too) 90 | - Phantom's reourceTimeout now set to match Horseman's timeout 91 | - Horseman will use Phantom from the phantomjs-prebuilt/phantomjs npm packages 92 | if installed (still overridden by phantomPath option) 93 | 94 | ### Fixed 95 | - .open() now rejects on fail - thanks @w33ble. 96 | - .crop() now works with zoomFactor other than 1 - thanks @wong2. 97 | - .waitForSelector() now uses JQuery for selectors (when avialable) 98 | - navigationRequested callback now called with correct parameters - 99 | thanks @grahamkennery. 100 | - Tabs/pages not opened by Horseman (e.g. by a link) 101 | are now correctly added to Horseman's list of tabs 102 | 103 | ### Updated 104 | - bluebird dependency to 3.0.1 105 | - node-phantom-simple dependency to 2.2.4 106 | - clone dependency to 1.0.2 107 | 108 | ## 2.8.2 - 2015-11-23 109 | ### Fixed 110 | - #74 - do not try to exit PhantomJS when it failed to initialize 111 | 112 | ## 2.8.1 - 2015-11-06 113 | ### Fixed 114 | - .upload() now works in PhantomJS 1.x line (still broken in 2.x line) - 115 | thanks @flashhhh 116 | - logging for falsy arguments - thanks @awlayton 117 | 118 | ## 2.8.0 - 2015-10-28 119 | ### Added 120 | - .cropBase64(). Thanks @jeprojects! 121 | 122 | ### Updated 123 | - node-phantom-simple dependency to version 2.1.0. 124 | - Updated readme with examples on how to set header/footer contents. 125 | 126 | ## 2.7.1 - 2015-10-21 127 | ### Fixed 128 | - If an exception was thrown, close() wouldn't get called. Merged #58. 129 | Thanks @awlayton! 130 | 131 | ## 2.7.0 - 2015-10-15 132 | ### Updated 133 | - Makes promise functions (like then, catch, tap) chainable with the rest of 134 | Horseman actions. Merges #57. Thanks @awlayton! 135 | 136 | ## 2.6.0 - 2015-10-08 137 | ### Updated 138 | - .close() is now chainable. Updated readme with examples. 139 | 140 | ## 2.5.0 - 2015-10-08 141 | ### Added 142 | - .log(). Closes #54. 143 | 144 | ### Fixed 145 | - .viewport() wasn't returning the actual viewport size. 146 | 147 | ## 2.4.0 - 2015-10-08 148 | ### Added 149 | - .do(). Closes #53. 150 | 151 | ## 2.3.0 - 2015-10-01 152 | ### Added 153 | - Support for multiple tabs. Closes #49. 154 | - .status(). Returns the HTTP status code from the last opened page. 155 | 156 | ## 2.2.1 - 2015-10-01 157 | ### Fixed 158 | - `.waitFor` bug fix. Closes #45. 159 | 160 | ## 2.2.0 - 2015-09-29 161 | ### Added 162 | - `.crop()`. Closes #51. 163 | 164 | ## 2.1.0 - 2015-09-27 165 | ### Added 166 | - API is chainable again, thanks to @awlayton. Addresses #46. 167 | 168 | ## 2.0.2 - 2015-09-21 169 | ### Fixed 170 | - `waitForNextPage` was broken. Fixes #44 (thanks @edge) 171 | 172 | ## 2.0.1 - 2015-09-14 173 | ### Upgraded 174 | - Moved to `node-phantom-simple` 2.0.4. 175 | 176 | ## 2.0.0 - 2015-09-10 177 | ### Changed 178 | - Complete API rewrite to use Promises. 179 | - Removed dependency on `deasync`. 180 | - Tab support removed, will add back in the future. 181 | 182 | ## 1.5.6 - 2015-08-17 183 | ### Fixed 184 | - #39. Upgraded to node-phantom-simple 2.0.3. 185 | 186 | ## 1.5.5 - 2015-08-12 187 | ### Changed 188 | - Upgraded to node-phantom-simple 2.0.2. Close #37. 189 | - Upgraded to Mocha 2.2.5 to remove "child_process: customFds option is 190 | deprecated, use stdio instead" message when testing. 191 | - Edited this document to reflect actual release dates from 1.4.1 - present. 192 | 193 | ## 1.5.4 - 2015-07-22 194 | ### Fixed 195 | - Merges #36 - Setting via horseman.value( newVal ) now fires a change event. 196 | Thanks @fpinzn. 197 | 198 | ## 1.5.3 - 2015-07-13 199 | ### Fixed 200 | - Fix #27 & 33 - .cookies() wasn't returning a list of cookies for the current 201 | page. Now fixed. 202 | 203 | ## 1.5.2 - 2015-07-01 204 | ### Fixed 205 | - Fix #30 - crop() now chainable. Thanks @jackstrain. 206 | 207 | ## 1.5.1 - 2015-03-10 208 | ### Fixed 209 | - Readme issue 210 | 211 | ## 1.5.0 - 2015-03-10 212 | ### Added 213 | - switchToChildFrame() (issue #18, thanks @easyrider) 214 | 215 | ## 1.4.1 - 2015-03-04 216 | ### Fixed 217 | - Readme issue 218 | 219 | ## 1.4.0 - 2015-03-04 220 | ### Added 221 | - keyboardEvent function 222 | - mouseEvent function 223 | - exposed `phantomPath` instantiation option 224 | 225 | ## 1.3.6 - 2015-03-03 226 | ### Added 227 | - status function. 228 | - post function. 229 | 230 | ## 1.3.5 - 2015-03-02 231 | ### Fixed 232 | - Updated documentation to address issues #12 and #14. 233 | 234 | ## 1.3.4 - 2015-02-27 235 | ### Fixed 236 | - Forgot to merge cookiesFile branch :( 237 | 238 | ## 1.3.3 - 2015-02-27 239 | ### Added 240 | - Exposes `cookiesFile` option. (Issue #8). 241 | 242 | ## 1.3.2 - 2015-02-27 243 | ### Fixed 244 | - Removed `weak` option from Readme.md (issue #10). 245 | - Fixed horseman.close() bug. (issue #11) 246 | 247 | ## 1.3.1 - 2015-02-27 248 | ### Fixed 249 | - Copyright years in LICENSE (thanks @fay-jai) 250 | - .waitForSelector() text in Readme.me (issue #7. thanks @bchr02) 251 | 252 | ## 1.3.0 - 2015-02-26 253 | ### Added 254 | - tabCount function. 255 | - switchToTab function. 256 | - openTab function. 257 | - tabCreated event. 258 | 259 | ## 1.2.2 - 2015-02-26 260 | ### Fixed 261 | - Phantom options, like `loadImages` were not being honored in 1.2.1. 262 | 263 | ## 1.2.1 - 2015-02-26 264 | ### Changed 265 | - Swapped out `phantom` for `node-phantom-simple` to fix some performance 266 | issues (see issue #6). This a major change internally, 267 | but does not change the API and all tests are passing. 268 | 269 | ## 1.2.0 - 2015-02-24 270 | ### Added 271 | - zoom function. 272 | - pdf function. 273 | - scrollTo function. 274 | - headers function. 275 | 276 | ## 1.1.0 - 2015-02-24 277 | ### Added 278 | - screenshotBase64 function. 279 | - CHANGELOG.md 280 | 281 | ## 1.0.0 - 2015-02-12 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 John Titus 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Horseman 2 | ========= 3 | 4 | **Horseman is no longer supported** 5 | 6 | Sorry, but the maintainers just don't have the time anymore. You may want to check out [puppeteer](https://github.com/GoogleChrome/puppeteer) or [nightmare](https://github.com/segmentio/nightmare). 7 | 8 | [![Build Status](https://travis-ci.org/johntitus/node-horseman.svg?branch=master)](https://travis-ci.org/johntitus/node-horseman) 9 | 10 | Horseman lets you run [PhantomJS](http://phantomjs.org/) from Node. 11 | 12 | Horseman has: 13 | 14 | * a simple chainable (Promise based) API, 15 | * an easy-to-use control flow (see the examples), 16 | * support for multiple tabs being open at the same time, 17 | * built in jQuery for easier page manipulation, 18 | * built in bluebird for easier in-browser async. 19 | 20 | ## Examples 21 | 22 | ### Search on Google 23 | 24 | ```js 25 | var Horseman = require('node-horseman'); 26 | var horseman = new Horseman(); 27 | 28 | horseman 29 | .userAgent('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0') 30 | .open('http://www.google.com') 31 | .type('input[name="q"]', 'github') 32 | .click('[name="btnK"]') 33 | .keyboardEvent('keypress', 16777221) 34 | .waitForSelector('div.g') 35 | .count('div.g') 36 | .log() // prints out the number of results 37 | .close(); 38 | ``` 39 | 40 | Save the file as `google.js`. Then, `node google.js`. 41 | 42 | ### Count Twitter Followers Concurrently 43 | 44 | ```js 45 | const Horseman = require('node-horseman'); 46 | const users = ['PhantomJS', 'nodejs']; 47 | 48 | users.forEach((user) => { 49 | const horseman = new Horseman(); 50 | horseman 51 | .open(`http://twitter.com/${user}`) 52 | .text('.ProfileNav-item--followers .ProfileNav-value') 53 | .then((text) => { 54 | console.log(`${user}: ${text}`); 55 | }) 56 | .close(); 57 | }); 58 | ``` 59 | 60 | Save the file as `twitter.js`. Then, `node twitter.js`. 61 | 62 | ### For longer examples, check out the Examples folder. 63 | 64 | ## Installation 65 | 66 | `npm install node-horseman` 67 | 68 | Note: Make sure PhantomJS is available in your path, 69 | you have the phantomjs-prebuilt/phantomjs npm package installed, 70 | or use the `phantomPath` option. 71 | 72 | ## API 73 | 74 | ### Setup 75 | 76 | #### new Horseman(options) 77 | 78 | Create a new instance that can navigate around the web. 79 | 80 | The available options are: 81 | 82 | * `timeout`: how long to wait for page loads or wait periods, 83 | default `5000` ms. 84 | * `interval`: how frequently to poll for page load state, default `50` ms. 85 | * `loadImages`: load all inlined images, default `true`. 86 | * `switchToNewTab`: switch to new tab when created, default `false`. 87 | * `diskCache`: enable disk cache, default `false`. 88 | * `diskCachePath`: location for the disk cache. *(requires PhantomJS 2.0.0 or above)* 89 | * `cookiesFile`: A file where to store/use cookies. 90 | * `ignoreSSLErrors`: ignores SSL errors, 91 | such as expired or self-signed certificate errors. 92 | * `sslProtocol`: sets the SSL protocol for secure connections 93 | `[sslv3|sslv2|tlsv1|any]`, default `any`. 94 | * `webSecurity`: enables web security and forbids cross-domain XHR. 95 | * `injectJquery`: whether jQuery is automatically loaded into each page. 96 | Default is `true`. 97 | If jQuery is already present on the page, it is not injected. 98 | * `injectBluebird`: whether bluebird is automatically loaded into each page. 99 | Default is `false`. 100 | If `true` and `Promise` is already present on the page, it is not injected. 101 | If `'bluebird'` it is always injected as Bluebird, 102 | whether Promise is present or not. 103 | * `bluebirdDebug`: whether or not to enable bluebird debug features. 104 | Default is `false`. 105 | If `true` non-minified bluebird is injected 106 | and long stack traces are enabled 107 | * `proxy`: specify the proxy server to use `address:port`, default not set. 108 | * `proxyType`: specify the proxy server type `[http|socks5|none]`, 109 | default not set. 110 | * `proxyAuth`: specify the auth information for the proxy `user:pass`, 111 | default not set. 112 | * `phantomPath`: If PhantomJS is not installed in your path, 113 | you can use this option to specify the executable's location. 114 | * `phantomOptions`: Explicit PhantomJS options, e.g. 115 | `{'ssl-certificates-path': 'ca.pem'}`. 116 | For a complete list refer to the [PhantomJS command line interface]( 117 | http://phantomjs.org/api/command-line.html). 118 | **These options have precedence over options implicitly set by Horseman.** 119 | * `debugPort`: Enable web inspector on specified port, default not set. 120 | * `debugAutorun`: Autorun on launch when in debug mode, default is true. 121 | 122 | ### Configuration 123 | 124 | #### .setProxy(ip, \[port\], \[type\], \[user, pass\]) 125 | 126 | Dynamically set proxy settings (***requires PhantomJS 2.0.0 or above***). 127 | The `ip` argument can either be the IP of the proxy server, 128 | or a URI of the form `type://user:pass@ip:port`. 129 | 130 | The `port` is optional and defaults to `80`. 131 | The `type` is optional and defaults to `'http'`. 132 | The `user` and `pass` are the optional username and password for authentication, 133 | by default no authentication is used. 134 | 135 | ### Cleanup 136 | 137 | Be sure to `.close()` each Horseman instance when you're done with it! 138 | 139 | #### .close() 140 | 141 | Closes the Horseman instance by shutting down PhantomJS. 142 | 143 | ### Navigation 144 | 145 | #### .open(url) 146 | 147 | Load the page at `url`. 148 | 149 | #### .post(url, postData) 150 | 151 | POST `postData` to the page at `url`. 152 | 153 | #### .put(url, putData) 154 | 155 | PUT `putData` to the page at `url`. 156 | 157 | #### .back() 158 | 159 | Go back to the previous page. 160 | 161 | #### .forward() 162 | 163 | Go forward to the next page. 164 | 165 | #### .status() 166 | 167 | The HTTP status code returned for the page just opened. 168 | 169 | #### .reload() 170 | 171 | Refresh the current page. 172 | 173 | #### .cookies(\[object|array of objects|string\]) 174 | 175 | Without any options, 176 | this function will return all the cookies inside the browser. 177 | 178 | ```js 179 | horseman 180 | .open('http://httpbin.org/cookies') 181 | .cookies() 182 | .log() // [] 183 | .close(); 184 | ``` 185 | 186 | You can pass in a cookie object to add to the cookie jar. 187 | 188 | ```js 189 | horseman 190 | .cookies({ 191 | name: 'test', 192 | value: 'cookie', 193 | domain: 'httpbin.org' 194 | }) 195 | .open('http://httpbin.org/cookies') 196 | .cookies() 197 | .then(function(cookies){ 198 | console.log(cookies); 199 | return horseman.close(); 200 | }); 201 | 202 | /* 203 | [ { domain: '.httpbin.org', 204 | httponly: false, 205 | name: 'test', 206 | path: '/', 207 | secure: false, 208 | value: 'cookie' } ] 209 | */ 210 | ``` 211 | 212 | You can pass in an array of cookie objects 213 | to reset all the cookies in the cookie jar 214 | (or pass an empty array to remove all cookies). 215 | 216 | ```js 217 | horseman 218 | .cookies([ 219 | { 220 | name : 'test2', 221 | value : 'cookie2', 222 | domain: 'httpbin.org' 223 | }, 224 | { 225 | name : 'test3', 226 | value : 'cookie3', 227 | domain: 'httpbin.org' 228 | }]) 229 | .open('http://httpbin.org/cookies') 230 | .cookies() 231 | .then(function(cookies){ 232 | console.log(cookies.length); // 2 233 | return horseman.close(); 234 | }); 235 | 236 | ``` 237 | 238 | [cookies.txt]: 239 | You can pass in the name of a [cookies.txt][] formatted file 240 | to reset all the cookies in the cookie jar 241 | to those contained in the file. 242 | 243 | ```js 244 | horseman 245 | .cookies('my-cookies.txt') 246 | .open('http://httpbin.org/cookies') 247 | .cookies() 248 | .then(function(cookies){ 249 | console.log(cookies); 250 | return horseman.close(); 251 | }); 252 | 253 | /* Cookies from my-cookies.txt (converted to the above object format) */ 254 | 255 | ``` 256 | 257 | #### .userAgent(userAgent) 258 | 259 | Set the `userAgent` used by PhantomJS. 260 | You have to set the userAgent before calling `.open()`. 261 | 262 | #### .headers(headers) 263 | 264 | Set the `headers` used when requesting a page. 265 | The headers are a javascript object. 266 | You have to set the headers before calling `.open()`. 267 | 268 | #### .authentication(user, password) 269 | 270 | Set the `user` and `password` for accessing a web page 271 | using basic authentication. 272 | Be sure to set it before calling `.open(url)`. 273 | 274 | ```js 275 | horseman 276 | .authentication('myUserName', 'myPassword') 277 | .open('http://httpbin.org/basic-auth/myUserName/myPassword') 278 | .html('pre') 279 | .then(function(body) { 280 | console.log(body); 281 | /* 282 | { 283 | "authenticated": true, 284 | "user": "myUserName" 285 | } 286 | */ 287 | return horseman.close(); 288 | }); 289 | ``` 290 | 291 | #### .viewport(width, height) 292 | 293 | Set the `width` and `height` of the viewport, useful for screenshotting. 294 | You have to set the viewport before calling `.open()`. 295 | 296 | #### .scrollTo(top, left) 297 | 298 | Scroll to a position on the page, 299 | relative to the top left corner of the document. 300 | 301 | #### .zoom(zoomFactor) 302 | 303 | Set the amount of zoom on a page. The default zoomFactor is 1. 304 | To zoom to 200%, use a zoomFactor of 2. 305 | Combine this with `viewport` to produce high DPI screenshots. 306 | 307 | ```js 308 | horseman 309 | .viewport(3200,1800) 310 | .zoom(2) 311 | .open('http://www.horsemanjs.org') 312 | .screenshot('big.png') 313 | .close(); 314 | ``` 315 | 316 | ### Evaluation 317 | 318 | Evaluation elements return information from the page. 319 | 320 | #### .title() 321 | 322 | Get the title of the current page. 323 | 324 | #### .url() 325 | 326 | Get the URL of the current page. 327 | 328 | #### .visible(selector) 329 | 330 | Determines if a selector is visible, or not, on the page. Returns a boolean. 331 | 332 | #### .exists(selector) 333 | 334 | Determines if the selector exists, or not, on the page. Returns a boolean. 335 | 336 | #### .count(selector) 337 | 338 | Counts the number of `selector` on the page. Returns a number. 339 | 340 | #### .html(\[selector\], \[file\]) 341 | 342 | Gets the HTML inside of an element. 343 | If no `selector` is provided, it returns the HTML of the entire page. 344 | If `file` is provided, the HTML will be written to that filename. 345 | 346 | #### .text(selector) 347 | 348 | Gets the text inside of an element. 349 | 350 | #### .plainText() 351 | 352 | Gets the plain text of the whole page (using PhantomJS's [`plainText`](http://phantomjs.org/api/webpage/property/plain-text.html) property). 353 | 354 | #### .value(selector, \[val\]) 355 | 356 | Get, or set, the value of an element. 357 | 358 | #### .attribute(selector, attribute) 359 | 360 | Gets an attribute of an element. 361 | 362 | #### .cssProperty(selector, property) 363 | 364 | Gets a CSS property of an element. 365 | 366 | #### .width(selector) 367 | 368 | Gets the width of an element. 369 | 370 | #### .height(selector) 371 | 372 | Gets the height of an element. 373 | 374 | #### .screenshot(path) 375 | 376 | Saves a screenshot of the current page to the specified `path`. 377 | Useful for debugging. 378 | 379 | #### .screenshotBase64(type) 380 | 381 | Returns a base64 encoded string representing the screenshot. 382 | Type must be one of 'PNG', 'GIF', or 'JPEG'. 383 | 384 | #### .crop(area, path) 385 | 386 | [getBoundingClientRect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect 387 | Takes a cropped screenshot of the page. 388 | `area` can be a string identifying an html element on the screen to crop to, 389 | or a [getBoundingClientRect][] object. 390 | 391 | #### .cropBase64(area, type) 392 | 393 | Returns a string representing a cropped, base64 encoded screenshot of the page. 394 | `area` can be a string identifying an html element on the screen to crop to, 395 | or a [getBoundingClientRect][] object. 396 | Type must be one of 'PNG', 'GIF', or 'JPEG'. 397 | 398 | #### .pdf(path, \[paperSize\]) 399 | 400 | [US Letter]: 401 | Renders the page as a PDF. 402 | The default paperSize is [US Letter][]. 403 | 404 | The `paperSize` object should be in either this format: 405 | 406 | ```js 407 | { 408 | width: '200px', 409 | height: '300px', 410 | margin: '0px' 411 | } 412 | ``` 413 | 414 | or this format 415 | 416 | ```js 417 | { 418 | format: 'A4', 419 | orientation: 'portrait', 420 | margin: '1cm' 421 | } 422 | ``` 423 | 424 | Supported formats are: `A3`, `A4`, `A5`, `Legal`, `Letter`, `Tabloid`. 425 | 426 | Orientation (`portrait`, `landscape`) is optional and defaults to 'portrait'. 427 | 428 | Supported dimension units are: 'mm', 'cm', 'in', 'px'. No unit means 'px'. 429 | 430 | You can create a header and footer like this: 431 | 432 | ```js 433 | horseman 434 | .open('http://www.amazon.com') 435 | .pdf('amazon.pdf', { 436 | format: 'Letter', 437 | orientation: 'portrait', 438 | margin: '0.5in', 439 | header: { 440 | height: '3cm', 441 | contents: function(pageNum, numPages) { 442 | if (pageNum == 1) { 443 | return ''; 444 | } 445 | return '

Header ' + pageNum + ' / ' + numPages + '

'; 446 | } 447 | }, 448 | footer: { 449 | height: '3cm', 450 | contents: function(pageNum, numPages) { 451 | if (pageNum == 1) { 452 | return ''; 453 | } 454 | return '

Footer ' + pageNum + ' / ' + numPages + '

'; 455 | } 456 | } 457 | }) 458 | .close() 459 | ``` 460 | 461 | #### .log() 462 | 463 | Outputs the results of the last call in the chain, or a string you provide, 464 | without breaking the chain. 465 | 466 | ```js 467 | horseman 468 | .open('http://www.google.com') 469 | .count('a') 470 | .log() // outputs the number of anchor tags 471 | .click('a') 472 | .log('clicked the button') //outputs the string 473 | .close(); 474 | ``` 475 | 476 | #### .do(fn) 477 | 478 | Run an function without breaking the chain. Works with asynchronous functions. 479 | Must call the callback when complete. 480 | 481 | ```js 482 | horseman 483 | .open('http://www.google.com') 484 | .do(function(done){ 485 | setTimeout(done,1000); 486 | }) 487 | .close(); 488 | ``` 489 | 490 | #### .evaluate(fn, \[arg1, arg2,...\]) 491 | 492 | Invokes `fn` on the page with args. On completion it returns a value. 493 | Useful for extracting information from the page. 494 | 495 | ```js 496 | horseman 497 | .open('http://en.wikipedia.org/wiki/Headless_Horseman') 498 | .evaluate( function(selector){ 499 | // This code is executed inside the browser. 500 | // It's sandboxed from Node, and has no access to anything 501 | // in Node scope, unless you pass it in, like we did with 'selector'. 502 | // 503 | // You do have access to jQuery, via $, automatically. 504 | return { 505 | height : $( selector ).height(), 506 | width : $( selector ).width() 507 | } 508 | }, '.thumbimage') 509 | .then(function(size){ 510 | console.log(size); 511 | return horseman.close(); 512 | }); 513 | ``` 514 | 515 | Can be used in an asynchronous way as well (with a node-style callback). 516 | This is similar to `.do`, but `fn` is invoked in the browser. 517 | 518 | ```js 519 | horseman 520 | .open('http://en.wikipedia.org/wiki/Headless_Horseman') 521 | .evaluate(function(ms, done){ 522 | var start = Date.now(); 523 | setTimeout(function() { 524 | done(null, Date.now() - start); 525 | // ^ Can pass an Error as first argument, 526 | // making evaluate action reject its Promise in Node. 527 | // Second argument is what the Promise will resolve to. 528 | }, ms); 529 | }, 100) 530 | .then(function(actualMs){ 531 | console.log(actualMs); 532 | }) 533 | .close(); 534 | ``` 535 | 536 | Lastly, if `fn` returns a Promise or thenable, 537 | it will be waited on and the action in Node will resolve/reject accordingly. 538 | 539 | ```js 540 | horseman 541 | .open('http://en.wikipedia.org/wiki/Headless_Horseman') 542 | .evaluate(function() { 543 | // Silly example for illustrative purposes. 544 | return Bluebird.delay(100).return('Hello World'); 545 | }) 546 | .then(function(mesg){ 547 | // Will log 'Hello World' after a roughly 100 ms delay. 548 | console.log(mesg); 549 | }) 550 | .close(); 551 | ``` 552 | 553 | #### .click(selector) 554 | 555 | Clicks the `selector` element once. 556 | 557 | #### .select(selector, value) 558 | 559 | Sets the value of a `select` element to `value`. 560 | 561 | #### .clear(selector) 562 | 563 | Sets the value of an element to `''`. 564 | 565 | #### .type(selector, text, \[options\]) 566 | 567 | Enters the `text` provided into the `selector` element. 568 | Options is an object containing `eventType` 569 | (`'keypress'`, `'keyup'`, `'keydown'`. Default is `'keypress'`) 570 | and `modifiers`, which is a string in the form of `ctrl+shift+alt`. 571 | 572 | #### .upload(selector, path) 573 | 574 | Specify the `path` to upload into a file input `selector` element. 575 | 576 | #### .download(url, \[path\], \[binary\]) 577 | 578 | Download the contents of `url`. 579 | If `path` is supplied the contents will be written there, 580 | otherwise this gets the contents. 581 | If `binary` is `true` it gets the contents as a node `Buffer`, 582 | otherwise it gets them as a string (`binary` defaults to `false`). 583 | 584 | ***Please note: binary downloads do not work correctly with PhantomJS 1.x*** 585 | 586 | 587 | #### .injectJs(file) 588 | 589 | Inject a JavaScript file onto the page. 590 | 591 | #### .includeJs(url) 592 | 593 | Include an external JavaScript script on the page via URL. 594 | 595 | #### .mouseEvent(type, \[x, y, \[button\]\]) 596 | 597 | Send a mouse event to the page. 598 | Each event is sent to the page as if it comes from real user interaction. 599 | `type` must be one of 600 | `'mouseup'`, `'mousedown'`, `'mousemove'`, `'doubleclick'`, or `'click'`, 601 | which is the default. 602 | `x` and `y` are optional 603 | and specify the location on the page to send the mouse event. 604 | `button` is also optional, and defaults to `'left'`. 605 | 606 | #### .keyboardEvent(type, key, \[modifier\]) 607 | 608 | [phantom keys]: 609 | Send a keyboard event to the page. 610 | Each event is sent to the page as if it comes from real user interaction. 611 | `type` must be one of `'keyup'`, `'keydown'`, or `'keypress'`, 612 | which is the default. 613 | `key` should be a numerical value from [this page][phantom keys]. 614 | For instance, to send an "enter" key press, 615 | use `.keyboardEvent('keypress',16777221)`. 616 | 617 | `modifier` is optional, and comes from this list: 618 | 619 | * 0x02000000: A Shift key on the keyboard is pressed 620 | * 0x04000000: A Ctrl key on the keyboard is pressed 621 | * 0x08000000: An Alt key on the keyboard is pressed 622 | * 0x10000000: A Meta key on the keyboard is pressed 623 | * 0x20000000: A keypad button is pressed 624 | 625 | To send a shift+p event, 626 | you would use `.keyboardEvent('keypress','p',0x02000000)`. 627 | 628 | ### Waiting 629 | 630 | These functions for the browser to wait for an event to occur. 631 | If the event does not occur before the timeout period 632 | (configurable via the options), 633 | a timeout event will be fired and the Promise for the action will reject. 634 | 635 | #### .wait(ms) 636 | 637 | Wait for `ms` milliseconds e.g. `.wait(5000)` 638 | 639 | #### .waitForNextPage([options]) 640 | 641 | Wait until a page finishes loading, typically after a `.click()`. 642 | `options` can have be `{timeout: 5000}` to define an action-specific timeout. 643 | 644 | #### .waitForSelector(selector, [options]) 645 | 646 | Wait until the element `selector` is present, 647 | e.g., `.waitForSelector('#pay-button')` 648 | 649 | `options` can have be `{timeout: 5000}` to define an action-specific timeout. 650 | 651 | #### .waitFor(fn, \[arg1, arg2,...\], value) 652 | 653 | Wait until the `fn` evaluated on the page returns the *specified* `value`. 654 | `fn` is invoked with args. 655 | 656 | ```js 657 | // This will call the function in the browser repeatedly 658 | // until true (or whatever else you specified) is returned 659 | horseman 660 | .waitFor(function waitForSelectorCount(selector, count) { 661 | return $(selector).length >= count 662 | }, '.some-selector', 2, true) 663 | // last argument (true here) is what return value to wait for 664 | ``` 665 | 666 | #### .waitFor(options) 667 | 668 | Alternative signature for .waitFor, with extra options. 669 | 670 | ```js 671 | // This will call the function in the browser repeatedly 672 | // until true (or whatever else you specified) is returned 673 | horseman 674 | .waitFor({ 675 | fn : function waitForSelectorCount(selector, count) { 676 | return $(selector).length >= count 677 | }, 678 | args : ['.some-selector', 2], 679 | value : true 680 | }) 681 | // last argument (true here) is what return value to wait for 682 | ``` 683 | 684 | `fn` : function to execute, mandatory 685 | `args` : `fn` arguments 686 | `value` : Wait until the `fn` evaluated on the page returns the *specified* `value`. 687 | `timeout` : specific timeout 688 | 689 | ### Frames 690 | 691 | #### .frameName() 692 | 693 | Get the name of the current frame. 694 | 695 | #### .frameCount() 696 | 697 | Get the number of frames inside the current frame. 698 | 699 | #### .frameNames() 700 | 701 | Get the names of the frames inside the current frame. 702 | 703 | #### .switchToFocusedFrame() 704 | 705 | Switch to the frame that is in focus. 706 | 707 | #### .switchToFrame(nameOrPosition) 708 | 709 | Switch to the frame specified by a frame name or a frame position. 710 | 711 | #### .switchToMainFrame() 712 | 713 | Switch to the main frame of the page. 714 | 715 | #### .switchToParentFrame() 716 | 717 | Switch to the parent frame of the current frame. 718 | Resolves to `true` it switched frames 719 | and `false` if it did not (i.e., the main frame was the current frame). 720 | 721 | ### Tabs 722 | 723 | Horseman supports multiple tabs being open at the same time. 724 | 725 | #### .openTab(url) 726 | 727 | Open a URL in a new tab. Fires a `tabCreated` event. 728 | Also, the newly created tab becomes the current tab. 729 | 730 | #### .tabCount() 731 | 732 | Returns the number of tabs currently open. 733 | 734 | #### .switchToTab(tabnumber) 735 | 736 | Switch to another tab. Count starts at 0. 737 | 738 | #### .closeTab(tabNum) 739 | 740 | Close an open tab. Count starts at 0. 741 | 742 | ### Events 743 | 744 | #### .on(event, callback) 745 | 746 | Respond to page events with the callback. 747 | Be sure to set these before calling `.open()`. 748 | The `callback` is evaluated in node. 749 | If you need to return from `callback`, you probably want `.at` instead. 750 | 751 | Supported events are: 752 | 753 | * `initialized` - callback() 754 | * `loadStarted` - callback() 755 | * `loadFinished` - callback(status) 756 | * `tabCreated` - callback(tabNum) 757 | * `tabClosed` - callback(tabNum) 758 | * `urlChanged` - callback(targetUrl) 759 | * `navigationRequested` - callback(url, type, willNavigate, main) 760 | * `resourceRequested` - callback(requestData, networkRequest) 761 | * `resourceReceived` - callback(response) 762 | * `consoleMessage` - callback(msg, lineNumber, sourceId) 763 | * `alert` - callback(msg) 764 | * `confirm` - callback(msg) 765 | * `prompt` - callback(msg, defaultValue) 766 | * `error` - callback(msg, trace) 767 | * `timeout` - callback(msg) - Fired when a wait timeout period elapses. 768 | 769 | [the full callbacks list for phantomjs]: 770 | For a more in depth description, see [the full callbacks list for phantomjs][]. 771 | 772 | ```js 773 | horseman 774 | .on('consoleMessage', function( msg ){ 775 | console.log(msg); 776 | }) 777 | ``` 778 | 779 | #### .at(event, callback) 780 | 781 | Respond to page events with the callback. 782 | Be sure to set these before calling `.open()`. 783 | The `callback` is evaluated in PhantomJS. 784 | If you do not need to return from `callback`, you probably want `.on` instead. 785 | 786 | Useful events are: 787 | 788 | * `confirm` - callback(msg) 789 | * `prompt` - callback(msg, defaultVal) 790 | * `filePicker` - callback(oldFile) 791 | 792 | For a more in depth description, see [the full callbacks list for phantomjs][]. 793 | 794 | ```js 795 | horseman 796 | .at('confirm', function(msg) { 797 | return msg === 'Like this?' ? true : false; 798 | }) 799 | ``` 800 | 801 | ### Extending Horseman 802 | 803 | You can add your own actions to horseman with `Horseman.registerAction`. 804 | Be sure to register all actions *before* calling the constructor. 805 | 806 | ```js 807 | Horseman.registerAction('size', function(selector) { 808 | // The function will be called with the Horseman instance as this 809 | var self = this; 810 | // Return the horseman chain, or any Promise 811 | return this 812 | .waitForSelector(selector) 813 | .then(function() { 814 | return { 815 | w: self.width(selector), 816 | h: self.height(selector) 817 | }; 818 | }) 819 | .props(); 820 | }); 821 | 822 | var horseman = new Horseman(); 823 | horseman 824 | .open('http://example.org') 825 | .size('body') 826 | .log() // { w: 400, h: 240 } 827 | .close(); 828 | ``` 829 | 830 | 831 | ### Yielding 832 | 833 | [co]: 834 | You can use yields with Horseman with a library like [co][]. 835 | 836 | ```js 837 | var Horseman = require('node-horseman'), 838 | co = require('co'); 839 | 840 | var horseman = new Horseman(); 841 | 842 | co(function *(){ 843 | yield horseman.open('http://www.google.com'); 844 | var title = yield horseman.title(); 845 | var numLinks = yield horseman.count('a'); 846 | console.log('Title: ' + title); //Google 847 | console.log('Num Links: ' + numLinks); //35 848 | yield horseman.close(); 849 | }).catch(function(e){ 850 | console.log(e) 851 | }); 852 | ``` 853 | 854 | If you use yields, you may need to use the harmony flag when you run your file: 855 | 856 | ```shell-session 857 | node --harmony test.js 858 | ``` 859 | 860 | ### Debug 861 | 862 | To run the same file with debugging output, 863 | run it like this `DEBUG=horseman node myfile.js`. 864 | 865 | This will print out some additional information about what's going on: 866 | 867 | ```shell-session 868 | horseman .setup() creating phantom instance 1 +0ms 869 | horseman load finished, injecting jquery and client scripts +401ms 870 | horseman injected jQuery +0ms 871 | horseman .open: http://www.google.com +66ms 872 | horseman .type() horseman into input[name='q'] +51ms 873 | ``` 874 | 875 | ### Tests 876 | 877 | [Mocha]: 878 | [Should]: 879 | Automated tests for Horseman itself are run using [Mocha][] and [Should][], 880 | both of which will be installed via `npm install`. 881 | To run Horseman's tests, just do `npm test`. 882 | 883 | When the tests are done, you'll see something like this: 884 | 885 | ```shell-session 886 | $ npm test 887 | 102 passing (42s) 888 | 2 pending 889 | 890 | ``` 891 | 892 | ## License (MIT) 893 | 894 | ``` 895 | WWWWWW||WWWWWW 896 | W W W||W W W 897 | || 898 | ( OO )__________ 899 | / | \ 900 | /o o| MIT \ 901 | \___/||_||__||_|| * 902 | || || || || 903 | _||_|| _||_|| 904 | (__|__|(__|__| 905 | ``` 906 | 907 | Copyright (c) John Titus 908 | 909 | Permission is hereby granted, free of charge, to any person obtaining a copy of 910 | this software and associated documentation files (the 'Software'), to deal in 911 | the Software without restriction, including without limitation the rights to 912 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 913 | of the Software, and to permit persons to whom the Software is furnished to do 914 | so, subject to the following conditions: 915 | 916 | The above copyright notice and this permission notice shall be included in all 917 | copies or substantial portions of the Software. 918 | 919 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 920 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 921 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 922 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 923 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 924 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 925 | SOFTWARE. 926 | -------------------------------------------------------------------------------- /examples/follow.js: -------------------------------------------------------------------------------- 1 | // Find the number of follower for each account. Spawns a new horseman for each user for faster results. 2 | var Horseman = require('node-horseman'); 3 | 4 | var users = ['PhantomJS', 5 | 'ariyahidayat', 6 | 'detronizator', 7 | 'KDABQt', 8 | 'lfranchi', 9 | 'jonleighton', 10 | '_jamesmgreene', 11 | 'Vitalliumm']; 12 | 13 | users.forEach( function( user ){ 14 | var horseman = new Horseman(); 15 | horseman 16 | .open('http://mobile.twitter.com/' + user) 17 | .text('.UserProfileHeader-stat--followers .UserProfileHeader-statCount') 18 | .then(function(text){ 19 | console.log( user + ': ' + text ); 20 | }) 21 | .finally(function(){ 22 | return horseman.close(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/links.js: -------------------------------------------------------------------------------- 1 | // Grab links from Google. 2 | var Horseman = require("node-horseman"); 3 | 4 | var horseman = new Horseman(); 5 | 6 | var links = []; 7 | 8 | function getLinks(){ 9 | return horseman.evaluate( function(){ 10 | // This code is executed in the browser. 11 | var links = []; 12 | $("div.g h3.r a").each(function( item ){ 13 | var link = { 14 | title : $(this).text(), 15 | url : $(this).attr("href") 16 | }; 17 | links.push(link); 18 | }); 19 | return links; 20 | }); 21 | } 22 | 23 | function hasNextPage(){ 24 | return horseman.exists("#pnnext"); 25 | } 26 | 27 | function scrape(){ 28 | 29 | return new Promise( function( resolve, reject ){ 30 | return getLinks() 31 | .then(function(newLinks){ 32 | 33 | links = links.concat(newLinks); 34 | 35 | if ( links.length < 30 ){ 36 | return hasNextPage() 37 | .then(function(hasNext){ 38 | if (hasNext){ 39 | return horseman 40 | .click("#pnnext") 41 | .wait(1000) 42 | .then( scrape ); 43 | } 44 | }); 45 | } 46 | }) 47 | .then( resolve ); 48 | }); 49 | } 50 | 51 | horseman 52 | .userAgent("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0") 53 | .open("http://www.google.com") 54 | .type("input[name='q']","horseman") 55 | .click("input[value='Google Search']") 56 | .keyboardEvent("keypress",16777221) 57 | .waitForSelector("div.g") 58 | .then( scrape ) 59 | .finally(function(){ 60 | console.log(links.length) 61 | horseman.close(); 62 | }); 63 | -------------------------------------------------------------------------------- /examples/pizza.js: -------------------------------------------------------------------------------- 1 | // Find pizza in Mountain View using Yelp 2 | var Horseman = require("node-horseman"); 3 | 4 | var horseman = new Horseman(); 5 | 6 | horseman 7 | .open('http://lite.yelp.com/search?find_desc=pizza&find_loc=94040&find_submit=Search') 8 | .text('address') 9 | .log() 10 | .close(); -------------------------------------------------------------------------------- /lib/HorsemanPromise.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Extend our copy of bluebird 4 | module.exports = require('bluebird/js/release/promise')(); 5 | -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var util = require('util'); 5 | var url = require('url'); 6 | var defaults = require('defaults'); 7 | var cookiesTxt = require('cookies.txt'); 8 | var dataUriToBuffer = require('data-uri-to-buffer'); 9 | var debug = require('debug')('horseman'); 10 | var debugv = require('debug')('horseman:verbose'); 11 | var HorsemanPromise = require('./HorsemanPromise'); 12 | var TimeoutError = HorsemanPromise.TimeoutError; 13 | 14 | 15 | //**************************************************// 16 | // Navigation 17 | //**************************************************// 18 | 19 | /** 20 | * Get or set the user agent for Phantom. 21 | * @param {string} [userAgent] - User agent to use. 22 | */ 23 | exports.userAgent = function(userAgent) { 24 | var self = this; 25 | return this.ready.then(function() { 26 | return new HorsemanPromise(function(resolve, reject) { 27 | if (!userAgent) { 28 | self.page.get('settings', function(err, settings) { 29 | debug('.userAgent() get'); 30 | if (err) { return reject(err); } 31 | return resolve(settings.userAgent); 32 | }); 33 | } else { 34 | self.page.get('settings', function(err, settings) { 35 | if (err) { return reject(err); } 36 | settings.userAgent = userAgent; 37 | self.page.set('settings', settings, function(err) { 38 | if (err) { return reject(err); } 39 | debug('.userAgent() set', userAgent); 40 | return resolve(); 41 | }); 42 | }); 43 | } 44 | }); 45 | }); 46 | }; 47 | 48 | /** 49 | * Open a url in Phantom. 50 | * @param {string} url 51 | * @param {string | object} [method=GET] - HTTP method, or settings object 52 | * @param {string} [data] 53 | * @see {@link http://phantomjs.org/api/webpage/method/open.html|PhantomJS API} 54 | */ 55 | exports.open = function(url, method) { 56 | var args = Array.prototype.slice.call(arguments); 57 | var self = this; 58 | self.targetUrl = url; 59 | 60 | return this.ready.then(function() { 61 | method = method || 'GET'; 62 | var meth = (typeof method === 'object') ? method.operation : method; 63 | meth = meth.toUpperCase(); 64 | 65 | if (args.length >= 2) { 66 | debug('.open()', meth, url); 67 | } else { 68 | debug('.open()', url); 69 | } 70 | var loaded = self.page.loadedPromise(); 71 | loaded.catch(function() {}); 72 | return HorsemanPromise.fromCallback(function(done) { 73 | args.push(done); 74 | return self.page.open.apply(self.page, args); 75 | }) 76 | .tap(function checkStatus(status) { 77 | if (status !== 'success') { 78 | var err = new Error('Failed to ' + meth + ' url: ' + url); 79 | return HorsemanPromise.reject(err); 80 | } 81 | }) 82 | .tap(function waitFoLoadFinished() { 83 | // Make sure page injecting is done 84 | return loaded; 85 | }); 86 | }); 87 | }; 88 | 89 | /** 90 | * Set headers sent to the remote server during an 'open'. 91 | * @param {Object[]} headers 92 | */ 93 | exports.headers = function(headers) { 94 | var self = this; 95 | 96 | return this.ready.then(function() { 97 | return HorsemanPromise.fromCallback(function(done) { 98 | debug('.headers()'); 99 | self.page.set('customHeaders', headers, done); 100 | }); 101 | }); 102 | }; 103 | 104 | /** 105 | * Go back a page. 106 | */ 107 | exports.back = function() { 108 | var self = this; 109 | return this.ready.then(function() { 110 | return HorsemanPromise.fromCallback(function(done) { 111 | debug('.back()'); 112 | self.page.goBack(done); 113 | }); 114 | }); 115 | }; 116 | 117 | /** 118 | * Go forwards a page. 119 | */ 120 | exports.forward = function() { 121 | var self = this; 122 | return this.ready.then(function() { 123 | return HorsemanPromise.fromCallback(function(done) { 124 | debug('.forward()'); 125 | self.page.goForward(done); 126 | }); 127 | }); 128 | }; 129 | 130 | /** 131 | * Use basic HTTP authentication when opening a page. 132 | * @param {string} user 133 | * @param {string} password 134 | */ 135 | exports.authentication = function(user, password) { 136 | var self = this; 137 | return this.ready.then(function() { 138 | return HorsemanPromise.fromCallback(function(done) { 139 | self.page.get('settings', function(err, settings) { 140 | if (err) { return done(err); } 141 | debug('.authentiation() set'); 142 | settings.userName = user; 143 | settings.password = password; 144 | self.page.set('settings', settings, done); 145 | }); 146 | }); 147 | }); 148 | }; 149 | 150 | /** 151 | * Get or set the size of the viewport. 152 | * @param {number} [width] 153 | * @param {number} [height] 154 | */ 155 | exports.viewport = function(width, height) { 156 | var self = this; 157 | if (!width) { 158 | debug('.viewport() get'); 159 | return this.ready.then(function() { 160 | return self.__evaluate(function getViewport() { 161 | return { 162 | width: window.innerWidth, 163 | height: window.innerHeight 164 | }; 165 | }); 166 | }); 167 | } else { 168 | debug('.viewport() set', width, height); 169 | return this.ready.then(function() { 170 | return HorsemanPromise.fromCallback(function(done) { 171 | var viewport = { 172 | width: width, 173 | height: height 174 | }; 175 | self.page.set('viewportSize', viewport, done); 176 | }); 177 | }); 178 | } 179 | }; 180 | 181 | /** 182 | * Set the zoom factor of the page. 183 | * @param {number} zoomFactor 184 | */ 185 | exports.zoom = function(zoomFactor) { 186 | var self = this; 187 | debug('.zoomFactor() set', zoomFactor); 188 | return this.ready.then(function() { 189 | return HorsemanPromise.fromCallback(function(done) { 190 | self.page.set('zoomFactor', zoomFactor, done); 191 | }); 192 | }); 193 | }; 194 | 195 | /** 196 | * Scroll to a position on the page. 197 | * @param {number} top 198 | * @param {number} left 199 | */ 200 | exports.scrollTo = function(top, left) { 201 | var self = this; 202 | 203 | var position = { 204 | top: top, 205 | left: left 206 | }; 207 | debug('.scrollTo()', top, left); 208 | return this.ready.then(function() { 209 | return new HorsemanPromise(function(resolve, reject) { 210 | self.page.set('scrollPosition', position, function(err) { 211 | if (err) { return reject(err); } 212 | return resolve(); 213 | }); 214 | }); 215 | }); 216 | }; 217 | 218 | /** 219 | * Send Post data to a url. 220 | * @param {string} url 221 | * @param {string} postData 222 | */ 223 | exports.post = function(url, postData) { 224 | debug('.post()', url); 225 | return this.open(url, 'POST', postData); 226 | }; 227 | 228 | /** 229 | * Send Put data to a url. 230 | * @param {string} url 231 | * @param {string} putData 232 | */ 233 | exports.put = function(url, putData) { 234 | debug('.put()', url); 235 | return this.open(url, 'PUT', putData); 236 | }; 237 | 238 | 239 | /** 240 | * Reload the page. 241 | */ 242 | exports.reload = function() { 243 | debug('.reload()'); 244 | return this.__evaluate(function reload() { 245 | document.location.reload(true); 246 | }); 247 | }; 248 | 249 | /** 250 | * Get or set the cookies for Phantom. 251 | * @param {object|object[]|string} arg - Cookie, array of cookies, 252 | * or cookies.txt file 253 | * @see {@link http://www.cookiecentral.com/faq/#3.5|cookies.txt format} 254 | */ 255 | exports.cookies = function(arg) { 256 | var self = this; 257 | return this.ready.then(function() { 258 | if (arg) { 259 | if (arg instanceof Array) { // replace all the cookies! 260 | return HorsemanPromise 261 | .fromCallback(function(done) { 262 | return self.phantom.clearCookies(done); 263 | }) 264 | .tap(function() { 265 | debug('.cookies() reset'); 266 | }) 267 | .return(arg) 268 | .each(function(cookie) { 269 | return HorsemanPromise.fromCallback(function(done) { 270 | self.phantom.addCookie(cookie, done); 271 | }); 272 | }); 273 | } 274 | switch (typeof arg) { 275 | // adding one cookie 276 | case 'object': 277 | return HorsemanPromise 278 | .fromCallback(function(done) { 279 | return self.phantom.addCookie(arg, done); 280 | }).tap(function() { 281 | debug('.cookies() added'); 282 | }); 283 | // replace all cookies with file cookies 284 | case 'string': 285 | return HorsemanPromise 286 | .fromCallback(function(done) { 287 | return self.phantom.clearCookies(done); 288 | }) 289 | .tap(function() { 290 | debug('.cookies() reset'); 291 | }) 292 | .then(function() { 293 | return HorsemanPromise.fromCallback(function(done) { 294 | return cookiesTxt.parse(arg, function(cookies) { 295 | return done(null, cookies); 296 | }); 297 | }); 298 | }) 299 | .each(function(cookie) { 300 | return HorsemanPromise.fromCallback(function(done) { 301 | self.phantom.addCookie(cookie, done); 302 | }); 303 | }); 304 | } 305 | } else { // return cookies for this page 306 | return HorsemanPromise 307 | .fromCallback(function(done) { 308 | return self.page.get('cookies', done); 309 | }) 310 | .tap(function() { 311 | debug('.cookies() returned'); 312 | }); 313 | } 314 | }); 315 | }; 316 | 317 | //**************************************************// 318 | // Interaction 319 | //**************************************************// 320 | 321 | /** 322 | * Save a screenshot to disk. 323 | * @param {string} path 324 | */ 325 | exports.screenshot = function(path) { 326 | var self = this; 327 | return this.ready.then(function() { 328 | return HorsemanPromise.fromCallback(function(done) { 329 | debug('.screenshot()', path); 330 | self.page.render(path, done); 331 | }); 332 | }); 333 | }; 334 | 335 | /** 336 | * Click on a selector and fire a 'click event'. 337 | * @param {string} selector 338 | */ 339 | exports.click = function(selector) { 340 | var self = this; 341 | 342 | debug('.click()', selector); 343 | return self.__evaluate(function click(selector) { 344 | var element; 345 | var event; 346 | if (window.jQuery) { 347 | element = jQuery(selector); 348 | event = document.createEvent('MouseEvent'); 349 | event.initEvent('click', true, true); 350 | element.get(0).dispatchEvent(event); 351 | } else { 352 | element = document.querySelector(selector); 353 | event = document.createEvent('MouseEvent'); 354 | event.initEvent('click', true, true); 355 | element.dispatchEvent(event); 356 | } 357 | }, selector) 358 | .tap(function() { debug('.click() done'); }); 359 | }; 360 | 361 | /** 362 | * Get the bounding rectangle of a selector. 363 | * @param {string} selector 364 | */ 365 | exports.boundingRectangle = function(selector) { 366 | var self = this; 367 | return this.__evaluate(function boundingRectangle(selector) { 368 | if (window.jQuery) { 369 | return jQuery(selector)[0].getBoundingClientRect(); 370 | } else { 371 | var element = document.querySelector(selector); 372 | return element.getBoundingClientRect(); 373 | } 374 | }, selector); 375 | }; 376 | 377 | /** 378 | * Save a cropped screenshot to disk. 379 | * @param {strning|object} area 380 | * @param {string} path 381 | * @see window.getBoundingRectangle 382 | */ 383 | exports.crop = function(area, path) { 384 | var self = this; 385 | function doCrop(area) { 386 | return self.ready 387 | .then(function getZoomFactor() { 388 | return HorsemanPromise.fromCallback(function(done) { 389 | return self.page.get('zoomFactor', done); 390 | }); 391 | }).then(function(zoomFactor) { 392 | var rect = { 393 | top: area.top * zoomFactor, 394 | left: area.left * zoomFactor, 395 | width: area.width * zoomFactor, 396 | height: area.height * zoomFactor 397 | }; 398 | return self.ready 399 | .then(function getClipRect() { 400 | return HorsemanPromise.fromCallback(function(done) { 401 | return self.page.get('clipRect', done); 402 | }); 403 | }) 404 | .tap(function setClipRect(prevClipRect) { 405 | return HorsemanPromise.fromCallback(function(done) { 406 | return self.page.set('clipRect', rect, done); 407 | }); 408 | }) 409 | .tap(function screenShot() { 410 | return self.screenshot(path); 411 | }); 412 | }) 413 | .then(function resetClipRect(prevClipRect) { 414 | return HorsemanPromise.fromCallback(function(done) { 415 | return self.page.set('clipRect', prevClipRect, done); 416 | }); 417 | }); 418 | } 419 | if (typeof area === 'string') { 420 | return this 421 | .boundingRectangle(area) 422 | .then(doCrop); 423 | } else { 424 | return doCrop(area); 425 | } 426 | }; 427 | 428 | /** 429 | * Take a base64 encoded cropped screenshot. 430 | * @param {string|object} area 431 | * @param {string} type - Type of image (e.g., PNG) 432 | * @see window.getBoundingRectangle 433 | */ 434 | exports.cropBase64 = function(area, type) { 435 | var self = this; 436 | function doCrop(area) { 437 | var b64; 438 | return self.ready 439 | .then(function getZoomFactor() { 440 | return HorsemanPromise.fromCallback(function(done) { 441 | return self.page.get('zoomFactor', done); 442 | }); 443 | }).then(function(zoomFactor) { 444 | var rect = { 445 | top: area.top * zoomFactor, 446 | left: area.left * zoomFactor, 447 | width: area.width * zoomFactor, 448 | height: area.height * zoomFactor 449 | }; 450 | return self.ready 451 | .then(function getClipRect() { 452 | return HorsemanPromise.fromCallback(function(done) { 453 | return self.page.get('clipRect', done); 454 | }); 455 | }) 456 | .tap(function setClipRect(prevClipRect) { 457 | return HorsemanPromise.fromCallback(function(done) { 458 | return self.page.set('clipRect', rect, done); 459 | }); 460 | }) 461 | .tap(function renderBase64() { 462 | b64 = self.screenshotBase64(type); 463 | return b64; 464 | }); 465 | }) 466 | .then(function resetClipRect(prevClipRect) { 467 | return HorsemanPromise.fromCallback(function(done) { 468 | return self.page.set('clipRect', prevClipRect, done); 469 | }); 470 | }) 471 | .then(function() { 472 | return b64; 473 | }); 474 | } 475 | if (typeof area === 'string') { 476 | return this 477 | .boundingRectangle(area) 478 | .then(doCrop); 479 | } else { 480 | return doCrop(area); 481 | } 482 | }; 483 | 484 | /** 485 | * Take a base64 encoded screenshot. 486 | * @param {string} type - Type of image (e.g., PNG) 487 | */ 488 | exports.screenshotBase64 = function(type) { 489 | if (['PNG', 'GIF', 'JPEG'].indexOf(type) == -1) { 490 | debug('.screenshotBase64() with type ' + type + ' not supported.'); 491 | debug('type must be one of PNG, GIF, or JPEG'); 492 | var err = new Error('screenshotBase64 type must be PNG, GIF, or JPEG.'); 493 | return HorsemanPromise.reject(err); 494 | } else { 495 | var self = this; 496 | var result; 497 | return this.ready.then(function() { 498 | return HorsemanPromise.fromCallback(function(done) { 499 | debug('.screenshotBase64()', type); 500 | return self.page.renderBase64(type, done); 501 | }); 502 | }); 503 | } 504 | }; 505 | 506 | /** 507 | * Save the current page as a pdf. 508 | * For more info - http://phantomjs.org/api/webpage/property/paper-size.html 509 | * @param {string} path - The name and location of where to store the pdf. 510 | * @param {object} [paperSize] - pdf's format, orientation, margin, and more. 511 | * @param {string} [paperSize.format] - Format of the pdf. 512 | * Supported formats are: 'A3', 'A4', 'A5', 'Legal', 'Letter', 'Tabloid'. 513 | * Default is 'letter' cuz 'Murica. 514 | * @param {string} [paperSize.orientation] - Orientation of the pdf. 515 | * Must be 'portrait' or 'landscape'. 516 | * Default is 'portrait'. 517 | * @param {string} [paperSize.margin] - Margin of the pdf. 518 | * Default is '0.5in'. 519 | */ 520 | exports.pdf = function(path, paperSize) { 521 | var self = this; 522 | debug('.pdf()', path, paperSize); 523 | if (!paperSize) { 524 | paperSize = { 525 | format: 'Letter', 526 | orientation: 'portrait', 527 | margin: '0.5in' 528 | }; 529 | } 530 | return this.ready 531 | .then(function setPaperSize() { 532 | return HorsemanPromise.fromCallback(function(done) { 533 | return self.page.set('paperSize', paperSize, done); 534 | }); 535 | }) 536 | .then(function render() { 537 | return HorsemanPromise.fromCallback(function(done) { 538 | return self.page.render(path, { 539 | format: 'pdf', 540 | quality: '100' 541 | }, done); 542 | }); 543 | }) 544 | .tap(function() { 545 | debug('.pdf() complete'); 546 | }); 547 | }; 548 | 549 | /** 550 | * Injects javascript from a file into the page. 551 | * @param {string} file - file containing javascript to inject onto the page. 552 | */ 553 | exports.injectJs = function(file) { 554 | var self = this; 555 | return this.ready 556 | .then(function() { 557 | return HorsemanPromise.fromCallback(function(done) { 558 | debug('.injectJs()', file); 559 | return self.page.injectJs(file, done); 560 | }); 561 | }) 562 | .tap(function(successful) { 563 | if (!successful) { 564 | var err = new Error('failed to inject ' + file); 565 | return HorsemanPromise.reject(err); 566 | } 567 | }); 568 | }; 569 | 570 | /** 571 | * Includes javascript script from a url on the page. 572 | * @param {string} url - The url to a javascript file to include o the page. 573 | */ 574 | exports.includeJs = function(url) { 575 | var self = this; 576 | return this.ready.then(function() { 577 | return HorsemanPromise.fromCallback(function(done) { 578 | debug('.includeJs()', url); 579 | return self.page.includeJs(url, done); 580 | }); 581 | }); 582 | }; 583 | 584 | /** 585 | * Select a value in an html select element. 586 | * @param {string} selector - The identifier for the select element. 587 | * @param {string} value - The value to select. 588 | */ 589 | exports.select = function(selector, value) { 590 | debug('.select()', selector, value); 591 | return this.value(selector, value); 592 | }; 593 | 594 | 595 | /** 596 | * Fire a key event. 597 | * @param {string} [type=keypress] - The type of key event. 598 | * @param {string} [key=null] - The key to use for the event. 599 | * @param {number} [modifier=0] - The keyboard modifier to use. 600 | * @see {@link http://phantomjs.org/api/webpage/method/send-event.html} 601 | */ 602 | exports.keyboardEvent = function(type, key, modifier) { 603 | type = (typeof type === 'undefined') ? 'keypress' : type; 604 | key = (typeof key === 'undefined') ? null : key; 605 | modifier = (typeof modifier === 'undefined') ? 0 : modifier; 606 | 607 | var self = this; 608 | return self.ready 609 | .then(function() { 610 | return HorsemanPromise.fromCallback(function(done) { 611 | self.page.sendEvent(type, key, null, null, modifier, done); 612 | }); 613 | }) 614 | .tap(function() { 615 | debug('.keyboardEvent()', type, key, modifier); 616 | }); 617 | }; 618 | 619 | /** 620 | * Fire a mouse event. 621 | * @param {string} [type=click] - The type of mouse event. 622 | * @param {number} [x=null] - The x location to fire the event at. 623 | * @param {number} [y=null] - The y location to fire the event at. 624 | * @param {string} [button=left] - The mouse button to use. 625 | */ 626 | exports.mouseEvent = function(type, x, y, button) { 627 | type = (typeof type === 'undefined') ? 'click' : type; 628 | x = (typeof x === 'undefined') ? null : x; 629 | y = (typeof y === 'undefined') ? null : y; 630 | button = (typeof button === 'undefined') ? 'left' : button; 631 | 632 | var self = this; 633 | return self.ready 634 | .then(function() { 635 | return HorsemanPromise.fromCallback(function(done) { 636 | self.page.sendEvent(type, x, y, button, done); 637 | }); 638 | }) 639 | .tap(function() { 640 | debug('.mouseEvent()', type, x, y, button); 641 | }); 642 | }; 643 | 644 | 645 | 646 | /** 647 | * Simulate a keypress on a selector 648 | * @param {string} selector - The selctor to type into. 649 | * @param {string} text - The text to type. 650 | * @param {object} options - Lets you send keys like control & shift 651 | */ 652 | exports.type = function(selector, text, options) { 653 | var DEFAULTS = { 654 | reset: false, // clear the field first 655 | eventType: 'keypress', // keypress, keyup, keydown 656 | keepFocus: false // if true, don't blur afterwards 657 | }; 658 | 659 | function computeModifier(modifierString) { 660 | var modifiers = { 661 | 'ctrl': 0x04000000, 662 | 'shift': 0x02000000, 663 | 'alt': 0x08000000, 664 | 'meta': 0x10000000, 665 | 'keypad': 0x20000000 666 | }; 667 | var modifier = 0; 668 | var checkKey = function(key) { 669 | if (key in modifiers) { return; } 670 | debug(key + 'is not a supported key modifier'); 671 | }; 672 | if (!modifierString) { return modifier; } 673 | var keys = modifierString.split('+'); 674 | keys.forEach(checkKey); 675 | return keys.reduce(function(acc, key) { 676 | return acc | modifiers[key]; 677 | }, modifier); 678 | } 679 | 680 | var modifiers = computeModifier(options && options.modifiers); 681 | var opts = defaults(options || {}, DEFAULTS); 682 | 683 | var self = this; 684 | 685 | debug('.type()', selector, text, options); 686 | return self 687 | .__evaluate(function focus(selector) { 688 | if (window.jQuery) { 689 | jQuery(selector).focus(); 690 | } else { 691 | document.querySelector(selector).focus(); 692 | } 693 | }, selector) 694 | .return(text) 695 | .call('split', '') 696 | .each(function sendKey(key) { 697 | return self 698 | .keyboardEvent(opts.eventType, key, null, null, modifiers); 699 | }); 700 | }; 701 | 702 | /** 703 | * Clear an input field. 704 | * @param {string} selector - The selctor to clear. 705 | */ 706 | exports.clear = function(selector) { 707 | debug('.clear()', selector); 708 | return this.value(selector, ''); 709 | }; 710 | 711 | 712 | /** 713 | * Upload a file to the page. 714 | * @param {string} selector - The selctor to to use the upload the file. 715 | * @param {string} file - The file to upload. 716 | */ 717 | exports.upload = function(selector, path) { 718 | var self = this; 719 | return this.ready 720 | .then(function() { 721 | return HorsemanPromise.fromCallback(function(done) { 722 | return fs.stat(path, done); 723 | }); 724 | }) 725 | .call('isFile') 726 | .then(function(isFile) { 727 | if (isFile) { 728 | return HorsemanPromise.fromCallback(function(done) { 729 | return self.page.uploadFile(selector, path, done); 730 | }); 731 | } else { 732 | debug('.upload() file path not valid.'); 733 | var err = new Error('File path for upload is not valid.'); 734 | return HorsemanPromise.reject(err); 735 | } 736 | }) 737 | .tap(function() { 738 | debug('.upload()', path, selector); 739 | }); 740 | }; 741 | 742 | /** 743 | * Dowload a URL. 744 | * @param {string} url - URL to download. 745 | * @param {string} [path] - File to write download to. 746 | * @param {boolean} [binary=false] - Whether the download is a binary file. 747 | * @param {string} [method=GET] - HTTP method to use. 748 | * @param {string} [data] - Request data to send. 749 | */ 750 | // TODO: Should horseman have a general AJAX action? 751 | exports.download = function(url, path, binary, method, data) { 752 | var args = Array.prototype.slice.call(arguments); 753 | method = method && method.toUpperCase() || 'GET'; 754 | debug.apply(debug, ['.download() start'].concat(args)); 755 | return this 756 | .evaluate(function download(url, binary, method, data, v, done) { 757 | var xhr = new XMLHttpRequest(); 758 | xhr.open(method, url, true); 759 | xhr.responseType = binary ? 'blob' : 'text'; 760 | xhr.setRequestHeader('Accept', '*/*, image/*'); 761 | xhr.addEventListener('load', function downloaded() { 762 | if (!(xhr.status >= 200 && xhr.status < 300)) { 763 | return done(new Error(xhr.response)); 764 | } 765 | return done(null, xhr.response); 766 | }); 767 | if (v) { 768 | xhr.addEventListener('progress', function(e) { 769 | console.log('dowload progess: ' + 100 * e.loaded / e.total); 770 | }); 771 | } 772 | xhr.addEventListener('error', function(evt) { 773 | setTimeout(function() { 774 | done(new Error('xhr error')); 775 | }, 0); 776 | }); 777 | xhr.send(data); 778 | }, url, binary, method, data, debugv.enabled) 779 | .tap(function writeFile(buffer) { 780 | if (path) { 781 | return HorsemanPromise.fromCallback(function(done) { 782 | return fs.writeFile(path, buffer, done); 783 | }); 784 | } 785 | }) 786 | .tap(function() { 787 | debug.apply(debug, ['.download() finish'].concat(args)); 788 | }); 789 | }; 790 | 791 | /** 792 | * Run javascript on the page. 793 | * @param {function} fn - The function to run. 794 | * @param {...*} [arguments] - The optional arguments to pass to 'fn'. 795 | */ 796 | exports.manipulate = function(/*fn, arg1, arg2, etc*/) { 797 | this.__evaluate.apply(this, arguments); 798 | return this; 799 | }; 800 | 801 | 802 | /** 803 | * Execute a function without breaking the api chain. 804 | * @param fn The function to run. Must call 'done()' when complete. 805 | */ 806 | exports.do = function(fn) { 807 | debug('.do()', fn.name || ''); 808 | return this.ready.then(function() { 809 | return new HorsemanPromise(function(resolve, reject) { 810 | fn(resolve); 811 | }); 812 | }); 813 | }; 814 | 815 | //**************************************************// 816 | // Information 817 | //**************************************************// 818 | 819 | /** 820 | * Run a javascript function on the current page 821 | * and optionally return the results. 822 | * @param {function} fn 823 | * @param {...*} [arguments] - The optional arguments to pass to 'fn'. 824 | */ 825 | exports.evaluate = function(fn) { 826 | var args = Array.prototype.slice.call(arguments, 1); 827 | var self = this; 828 | var hasCb = args.length < fn.length; // Whether fn takes a callback 829 | var fname = fn.name || ''; 830 | 831 | debug.apply(debug, ['.evaluate()', fname].concat(args)); 832 | return this.ready.then(function() { 833 | // Handle this.page changing 834 | var page = self.page; 835 | var evaluate = HorsemanPromise.promisify(page.evaluate).bind(page); 836 | 837 | // TODO: Move evaluate wrapper to a client script? 838 | // Make dumy error to make evaluate rejections more debuggable 839 | var stack = HorsemanPromise.reject(new Error('See next line')); 840 | var res = evaluate(function evaluate(fnstr, hasCb, args) { 841 | if(!window.__horseman) { 842 | window.__horseman = {}; 843 | } 844 | __horseman.cbargs = undefined; 845 | var done = function done(err, res) { 846 | if (__horseman.cbargs) { return; } 847 | var iserr = err instanceof Error; 848 | if (iserr) { 849 | var keys = Object.getOwnPropertyNames(err); 850 | err = keys.reduce(function copyErr(obj, key) { 851 | obj[key] = err[key]; 852 | return obj; 853 | }, {}); 854 | } 855 | var isblob = res instanceof Blob; 856 | if (isblob) { 857 | var reader = new FileReader(); 858 | reader.onload = function converted() { 859 | res = reader.result; 860 | ddone(); 861 | }; 862 | reader.onerror = function() { 863 | setTimeout(function() { 864 | err = err || new Error('blob reader error'); 865 | ddone(); 866 | }, 0); 867 | }; 868 | reader.readAsDataURL(res); 869 | } else { 870 | ddone(); 871 | } 872 | function ddone() { 873 | __horseman.cbargs = { 874 | err: err, 875 | iserr: iserr, 876 | res: res, 877 | isblob: isblob 878 | }; 879 | } 880 | }; 881 | try { 882 | var fn; 883 | eval('fn = ' + fnstr); 884 | 885 | if (hasCb) { 886 | // Call fn asynchronously 887 | setTimeout(function() { 888 | try { 889 | return fn.apply(this, args.concat(done)); 890 | } catch (err) { 891 | return done(err); 892 | } 893 | }, 0); 894 | return 'c'; 895 | } else { 896 | var p; 897 | // Call fn synchronously 898 | p = fn.apply(this, args); 899 | 900 | if (p && typeof p.then === 'function') { 901 | // fn returned a Promise 902 | p.then(function onResolve(res) { 903 | return done(null, res); 904 | }, function onReject(err) { 905 | return done(err); 906 | }); 907 | return 'p'; 908 | } else { 909 | done(null, p); 910 | } 911 | } 912 | } catch (err) { 913 | done(err); 914 | } 915 | return 's'; 916 | }, fn.toString(), hasCb, args) 917 | .then(function waitForCb(type) { 918 | if (type !== 's') { 919 | debugv('.evaluate() waiting for callback', fname, type, 920 | self.id); 921 | return waitForPage.call(self, page, function waitForCb() { 922 | return !!__horseman.cbargs; 923 | }, true); 924 | } 925 | debugv('.evaluate() finished synchronously', fname, type, 926 | self.id); 927 | }) 928 | .then(function handleCb() { 929 | return evaluate(function handleCb() { 930 | return __horseman.cbargs; 931 | }); 932 | }) 933 | .then(function handleErrback(args) { 934 | args = args || {}; 935 | return stack.catch(function fixErr(err) { 936 | // TODO: Phantom 2 errors are different than 1 937 | if (args.err) { 938 | // Make long/normal stack traces work 939 | if (args.iserr) { 940 | args.err.name = args.err.name || 'Error'; 941 | 942 | if (!args.err.stack) { 943 | args.err.stack = args.err.toString(); 944 | } 945 | args.err.stack.replace(/\n*$/g, '\n'); 946 | var stack = err.stack.split('\n').slice(1); 947 | // Append Node stack to Phantom stack 948 | args.err.stack += stack.join('\n'); 949 | } 950 | if (args.err.stack) { 951 | args.err.toString = function() { 952 | return this.name + ': ' + this.message; 953 | }; 954 | } 955 | return HorsemanPromise.reject(args.err); 956 | } 957 | if (args.isblob) { 958 | return dataUriToBuffer(args.res); 959 | } 960 | return args.res; 961 | }); 962 | }); 963 | stack.catch(function() {}); 964 | return res; 965 | }); 966 | }; 967 | 968 | /** 969 | * Syncronous only version of evaluate, handles throws. 970 | * Should probably only be used internally. 971 | * @param {function} fn 972 | * @param {...*} [arguments] 973 | */ 974 | exports.__evaluate = function() { 975 | var args = Array.prototype.concat.apply([this.page], arguments); 976 | return evaluatePage.apply(this, args); 977 | }; 978 | /** 979 | * Evaluates a function on the given page. 980 | * @this Horseman 981 | * @param {Page} page 982 | * @param {function} fn 983 | * @param {...*} [arguments] 984 | */ 985 | function evaluatePage(page, fn) { 986 | var args = Array.prototype.slice.call(arguments, 2); 987 | return this.ready.then(function() { 988 | var stack; 989 | page = page || this.page; 990 | var res = HorsemanPromise.fromCallback(function(done) { 991 | // Wrap fn to be able to catch exceptions and reject Promise 992 | stack = HorsemanPromise.reject(new Error('See next line')); 993 | return page.evaluate(function evaluatePage(fnstr, args) { 994 | try { 995 | var fn; 996 | eval('fn = ' + fnstr); 997 | 998 | var res = fn.apply(this, args); // Call fn with args 999 | return { res: res }; 1000 | } catch (err) { 1001 | return { err: err, iserr: err instanceof Error }; 1002 | } 1003 | }, fn.toString(), args, done); 1004 | }) 1005 | .then(function handleErrback(args) { 1006 | return stack.catch(function(err) { 1007 | if (args.err) { 1008 | if (args.iserr) { 1009 | var stack = err.stack.split('\n').slice(1); 1010 | // Append Node stack to Phantom stack 1011 | args.err.stack += '\n' + stack.join('\n'); 1012 | } 1013 | return HorsemanPromise.reject(args.err); 1014 | } 1015 | return args.res; 1016 | }); 1017 | }); 1018 | stack.catch(function() {}); 1019 | return res; 1020 | }); 1021 | } 1022 | 1023 | 1024 | /** 1025 | * Get the url of the current page. 1026 | */ 1027 | exports.url = function() { 1028 | debug('.url()'); 1029 | return this.__evaluate(function url() { 1030 | return document.location.href; 1031 | }); 1032 | }; 1033 | 1034 | /** 1035 | * Count the number of occurances of 'selector' on the page. 1036 | * @param {string} selector 1037 | */ 1038 | exports.count = function(selector) { 1039 | debug('.count()', selector); 1040 | return this.__evaluate(function count(selector) { 1041 | var matches = (window.jQuery) ? 1042 | jQuery(selector) : document.querySelectorAll(selector); 1043 | return matches.length; 1044 | }, selector); 1045 | }; 1046 | 1047 | /** 1048 | * Get the title of the current page. 1049 | */ 1050 | exports.title = function() { 1051 | debug('.title()'); 1052 | return this.__evaluate(function title() { 1053 | return document.title; 1054 | }); 1055 | }; 1056 | 1057 | 1058 | /** 1059 | * Determine if the selector exists, at least once, on the page. 1060 | * @param {string} [selector] 1061 | */ 1062 | exports.exists = function(selector) { 1063 | debug('.exists()', selector); 1064 | return this.count(selector).then(function(count) { 1065 | return count > 0; 1066 | }); 1067 | }; 1068 | 1069 | /** 1070 | * Get the HTML for the page, or optionally for a selector. 1071 | * @param {string} [selector] 1072 | * @param {string} [file] - File to which to write the HTML. 1073 | */ 1074 | exports.html = function(selector, file) { 1075 | debug('.html()', selector); 1076 | return this 1077 | .__evaluate(function html(selector) { 1078 | if (selector) { 1079 | return (window.jQuery) ? 1080 | jQuery(selector).html() : 1081 | document.querySelector(selector).innerHTML; 1082 | } else { 1083 | return (window.jQuery) ? 1084 | jQuery('html').html() : 1085 | document.documentElement.innerHTML; 1086 | } 1087 | }, selector) 1088 | .tap(function writeHtmlToFile(html) { 1089 | if (file) { 1090 | return HorsemanPromise.fromCallback(function(done) { 1091 | return fs.writeFile(file, html, done); 1092 | }); 1093 | } 1094 | }); 1095 | }; 1096 | 1097 | /** 1098 | * Get the text for the body of the page, or optionally for a selector. 1099 | * @param {string} [selector] 1100 | */ 1101 | exports.text = function(selector) { 1102 | debug('.text()', selector); 1103 | return this.__evaluate(function text(selector) { 1104 | if (selector) { 1105 | return (window.jQuery) ? 1106 | jQuery(selector).text() : 1107 | document.querySelector(selector).textContent; 1108 | } else { 1109 | return (window.jQuery) ? 1110 | jQuery('body').text() : 1111 | document.querySelector('body').textContent; 1112 | } 1113 | }, selector); 1114 | }; 1115 | 1116 | /** 1117 | * Get the plain text for the body of the page. 1118 | */ 1119 | exports.plainText = function() { 1120 | var self = this; 1121 | return this.ready.then(function() { 1122 | return HorsemanPromise 1123 | .fromCallback(function(done) { 1124 | return self.page.get('plainText', done); 1125 | }); 1126 | }); 1127 | }; 1128 | 1129 | /** 1130 | * Get the value of an attribute for a selector. 1131 | * @param {string} selector 1132 | * @param {string} attr 1133 | */ 1134 | exports.attribute = function(selector, attr) { 1135 | debug('.attribute()', selector, attr); 1136 | return this.__evaluate(function attribute(selector, attr) { 1137 | return (window.jQuery) ? 1138 | jQuery(selector).attr(attr) : 1139 | document.querySelector(selector).getAttribute(attr); 1140 | }, selector, attr); 1141 | }; 1142 | 1143 | /** 1144 | * Get the value of an css property of a selector. 1145 | * @param {string} selector 1146 | * @param {string} prop 1147 | */ 1148 | exports.cssProperty = function(selector, prop) { 1149 | debug('.cssProperty()', selector, prop); 1150 | return this.__evaluate(function cssProperty(selector, prop) { 1151 | return (window.jQuery) ? 1152 | jQuery(selector).css(prop) : 1153 | window.getComputedStyle(document.querySelector(selector))[prop]; 1154 | }, selector, prop); 1155 | }; 1156 | 1157 | /** 1158 | * Get the width of an element. 1159 | * @param {string} selector 1160 | */ 1161 | exports.width = function(selector) { 1162 | debug('.width()', selector); 1163 | return this.__evaluate(function width(selector) { 1164 | return (window.jQuery) ? 1165 | jQuery(selector).width() : 1166 | document.querySelector(selector).offsetWidth; 1167 | }, selector); 1168 | }; 1169 | 1170 | /** 1171 | * Get the height of an element. 1172 | * @param {string} selector 1173 | */ 1174 | exports.height = function(selector) { 1175 | debug('.height()', selector); 1176 | return this.__evaluate(function height(selector) { 1177 | return (window.jQuery) ? 1178 | jQuery(selector).height() : 1179 | document.querySelector(selector).offsetHeight; 1180 | }, selector); 1181 | }; 1182 | 1183 | /** 1184 | * Get or set the value of an element. 1185 | * @param {string} selector - The selector to find or set the value of. 1186 | * @param {string} [value] - The value to set the selector to. 1187 | */ 1188 | exports.value = function(selector, value) { 1189 | debug('.value()', selector, value); 1190 | var self = this; 1191 | if (typeof value === 'undefined') { // get the value of an element 1192 | return self 1193 | .__evaluate(function valueGet(selector) { 1194 | return (window.jQuery) ? 1195 | jQuery(selector).val() : 1196 | document.querySelector(selector).value; 1197 | }, selector) 1198 | .then(function(val) { 1199 | if (val === null) { 1200 | return this 1201 | .exists(selector) 1202 | .then(function(exists) { 1203 | return (exists) ? '' : val; 1204 | }); 1205 | } else { 1206 | return val; 1207 | } 1208 | 1209 | }); 1210 | 1211 | } else { // set the value of an element 1212 | return self.__evaluate(function valueSet(selector, value) { 1213 | if (window.jQuery) { 1214 | jQuery(selector).val(value).change(); 1215 | } else { 1216 | var element = document.querySelector(selector); 1217 | var event = document.createEvent('HTMLEvents'); 1218 | element.value = value; 1219 | event.initEvent('change', true, true); 1220 | element.dispatchEvent(event); 1221 | } 1222 | }, selector, value); 1223 | } 1224 | }; 1225 | 1226 | /** 1227 | * Determines if an element is visible. 1228 | * @param {string} selector - The selector to find the visibility of. 1229 | */ 1230 | exports.visible = function(selector) { 1231 | debug('.visible()', selector); 1232 | return this 1233 | .__evaluate(function visible(selector) { 1234 | if (window.jQuery) { 1235 | return jQuery(selector).is(':visible'); 1236 | } else { 1237 | var elem = document.querySelector(selector); 1238 | return elem && (elem.offsetWidth > 0 && elem.offsetHeight > 0); 1239 | } 1240 | }, selector) 1241 | .then(function(vis) { 1242 | return vis || false; 1243 | }); 1244 | }; 1245 | 1246 | /** 1247 | * Log the output from either a previous chain method, 1248 | * or a string the user passed in. 1249 | * @param output 1250 | */ 1251 | exports.log = function(output) { 1252 | if (arguments.length === 0) { 1253 | output = this.lastVal; 1254 | } 1255 | 1256 | return this.ready.then(function() { 1257 | console.log(output); 1258 | return this.lastVal; 1259 | }); 1260 | }; 1261 | 1262 | //**************************************************// 1263 | // Tabs 1264 | //**************************************************// 1265 | 1266 | exports.tabCount = function() { 1267 | var self = this; 1268 | return this.ready.then(function() { 1269 | return self.tabs.length; 1270 | }); 1271 | }; 1272 | 1273 | /** 1274 | * Switch to another of the open tabs 1275 | * @param {integer} tabNumber - The number of the tab to switch to (from 0) 1276 | */ 1277 | exports.switchToTab = function(tabNumber) { 1278 | return this.ready.then(function() { 1279 | this.page = this.tabs[tabNumber]; 1280 | }); 1281 | }; 1282 | 1283 | /** 1284 | * Open URL in a new tab 1285 | * @param url - the URL to open in the newly created tab 1286 | */ 1287 | exports.openTab = function(url) { 1288 | var self = this; 1289 | self.targetUrl = url; 1290 | return this.ready.then(function() { 1291 | return self.pageMaker(url); 1292 | }); 1293 | }; 1294 | 1295 | /** 1296 | * Close a tab and release its resources. 1297 | * @param {integer} tabNumber - The number of the tab to close. 1298 | */ 1299 | exports.closeTab = function(tabNumber) { 1300 | var self = this; 1301 | debug('.closeTab()', tabNumber); 1302 | return this.ready.then(function() { 1303 | return HorsemanPromise.fromCallback(function(done) { 1304 | return self.tabs[tabNumber].close(done); 1305 | }); 1306 | }); 1307 | }; 1308 | 1309 | //**************************************************// 1310 | // Frames 1311 | //**************************************************// 1312 | 1313 | /** 1314 | * Get the name of the current frame. 1315 | */ 1316 | exports.frameName = function() { 1317 | var self = this; 1318 | return this.ready.then(function() { 1319 | return HorsemanPromise.fromCallback(function(done) { 1320 | debug('.frameName()'); 1321 | return self.page.get('frameName', done); 1322 | }); 1323 | }); 1324 | }; 1325 | 1326 | /** 1327 | * Get the count of frames inside the current frame. 1328 | */ 1329 | exports.frameCount = function() { 1330 | var self = this; 1331 | return this.ready.then(function() { 1332 | return HorsemanPromise.fromCallback(function(done) { 1333 | debug('.frameCount()'); 1334 | return self.page.get('framesCount', done); 1335 | }); 1336 | }); 1337 | }; 1338 | 1339 | /** 1340 | * Get the names of the frames inside the current frame. 1341 | */ 1342 | exports.frameNames = function() { 1343 | var self = this; 1344 | return this.ready.then(function() { 1345 | return HorsemanPromise.fromCallback(function(done) { 1346 | debug('.frameNames()'); 1347 | return self.page.get('framesName', done); 1348 | }); 1349 | }); 1350 | }; 1351 | 1352 | /** 1353 | * Switch to the focused frame. 1354 | */ 1355 | exports.switchToFocusedFrame = function() { 1356 | var self = this; 1357 | return this.ready.then(function() { 1358 | return HorsemanPromise.fromCallback(function(done) { 1359 | debug('.switchToFocusedFrame()'); 1360 | return self.page.switchToFocusedFrame(done); 1361 | }); 1362 | }); 1363 | }; 1364 | 1365 | /** 1366 | * Switch to a frame inside the current frame. 1367 | * @param {string|integer} nameOrPosition - Name or position of frame 1368 | * to switch to 1369 | */ 1370 | exports.switchToFrame = function(nameOrPosition) { 1371 | var self = this; 1372 | return this.ready.then(function() { 1373 | return HorsemanPromise.fromCallback(function(done) { 1374 | debug('.switchToFrame()', nameOrPosition); 1375 | return self.page.switchToFrame(nameOrPosition, done); 1376 | }); 1377 | }); 1378 | }; 1379 | /** 1380 | * Switch to a child frame. 1381 | * @deprecated Use switchToFrame instead. 1382 | * @param {string|integer} nameOrPosition - Name or position of frame 1383 | * to switch to 1384 | */ 1385 | exports.switchToChildFrame = util.deprecate(function(nameOrPosition) { 1386 | var self = this; 1387 | return this.ready.then(function() { 1388 | return HorsemanPromise.fromCallback(function(done) { 1389 | debug('.switchToChildFrame()', nameOrPosition); 1390 | return self.page.switchToChildFrame(nameOrPosition, done); 1391 | }); 1392 | }); 1393 | }, 'Horseman#switchToChildFrame: Use Horseman#switchToFrame instead'); 1394 | 1395 | /** 1396 | * Switch to the main frame. 1397 | */ 1398 | exports.switchToMainFrame = function() { 1399 | var self = this; 1400 | return this.ready.then(function() { 1401 | return HorsemanPromise.fromCallback(function(done) { 1402 | debug('.switchToMainFrame()'); 1403 | return self.page.switchToMainFrame(done); 1404 | }); 1405 | }); 1406 | }; 1407 | 1408 | /** 1409 | * Switch to the parent frame of the current frame. 1410 | */ 1411 | exports.switchToParentFrame = function() { 1412 | var self = this; 1413 | return this.ready.then(function() { 1414 | return HorsemanPromise.fromCallback(function(done) { 1415 | debug('.switchToParentFrame()'); 1416 | return self.page.switchToParentFrame(done); 1417 | }); 1418 | }); 1419 | }; 1420 | 1421 | /** 1422 | * Get the HTTP status of the last opened page. 1423 | */ 1424 | exports.status = function() { 1425 | var self = this; 1426 | return this.ready.then(function() { 1427 | var status = self.responses[ self.targetUrl ]; 1428 | 1429 | // If the open was for 'http://www.google.com', 1430 | // the results may be stored in 'http://www.google.com/' 1431 | if (typeof status === 'undefined') { 1432 | status = self.responses[ self.targetUrl + '/']; 1433 | } 1434 | return status; 1435 | }); 1436 | }; 1437 | 1438 | //**************************************************// 1439 | // Callbacks 1440 | //**************************************************// 1441 | /** 1442 | * Handles page events. 1443 | * @param {String} eventType 1444 | * @param {Function} callback 1445 | * 1446 | * eventType can be one of: 1447 | * initialized - callback() 1448 | * loadStarted - callback() 1449 | * loadFinished - callback(status) 1450 | * tabCreated - callback(tabNum, tab) 1451 | * tabClosed - callback(tabNum, tab) 1452 | * urlChanged - callback(targetUrl) 1453 | * navigationRequested - callback(url, type, willNavigate, main) 1454 | * resourceRequested - callback(requestData, networkRequest) 1455 | * resourceReceived - callback(response) 1456 | * pageCreated - callback(newPage) 1457 | * consoleMessage(msg, lineNum, sourceId) 1458 | * alert - callback(msg) 1459 | * confirm - callback(msg) 1460 | * prompt - callback(msg, defaultVal) 1461 | * filePicker - callback(oldFile) 1462 | * error - callback(msg, trace); 1463 | * timeout - callback(type) 1464 | */ 1465 | exports.on = function(eventType, callback) { 1466 | var self = this; 1467 | return this.ready.then(function() { 1468 | switch (eventType) { 1469 | /** 1470 | * Horseman events 1471 | */ 1472 | case 'timeout': 1473 | self.page.onTimeout = callback; 1474 | break; 1475 | case 'tabCreated': 1476 | self.onTabCreated = callback; 1477 | break; 1478 | case 'tabClosed': 1479 | self.onTabClosed = callback; 1480 | break; 1481 | 1482 | /** 1483 | * PhantomJS events 1484 | */ 1485 | // Callback horseman needs to mess with 1486 | case 'resourceTimeout': 1487 | self.page.onResouceTimeout = function(request) { 1488 | callback.apply(this, arguments); 1489 | // A resourceTimeout is a timeout 1490 | setTimeout(function() { 1491 | self.page.onTimeout('resourceTimneout', request); 1492 | }, 0); 1493 | }; 1494 | break; 1495 | case 'urlChanged': 1496 | self.page.onUrlChanged = function(targetUrl) { 1497 | self.targetUrl = targetUrl; 1498 | return callback.apply(this, arguments); 1499 | }; 1500 | break; 1501 | case 'resourceReceived': 1502 | self.page.onResourceReceived = function(response) { 1503 | self.responses[response.url] = response.status; 1504 | return callback.apply(this, arguments); 1505 | }; 1506 | break; 1507 | case 'pageCreated': 1508 | self.page.onPageCreated2 = callback; 1509 | break; 1510 | case 'loadFinished': 1511 | self.page.onLoadFinished2 = callback; 1512 | break; 1513 | // Others 1514 | default: 1515 | var pageEvent = 'on' + 1516 | eventType.charAt(0).toUpperCase() + eventType.slice(1); 1517 | self.page[pageEvent] = callback; 1518 | } 1519 | 1520 | debug('.on() ' + eventType + ' set.'); 1521 | }); 1522 | }; 1523 | /** 1524 | * Handle page events inside PhantomJS 1525 | * Phantom receives callback return value with .at but not with .on 1526 | * @see on 1527 | */ 1528 | exports.at = function(eventType, callback) { 1529 | return this.ready.then(function() { 1530 | var pageEvent = 'on' + 1531 | eventType.charAt(0).toUpperCase() + eventType.slice(1); 1532 | 1533 | this.page.setFn(pageEvent, callback); 1534 | 1535 | debug('.at() ' + eventType + ' set.'); 1536 | }); 1537 | }; 1538 | 1539 | //**************************************************// 1540 | // Waiting 1541 | //**************************************************// 1542 | 1543 | /** 1544 | * Wait for a specified period of time 1545 | * @param {number} miliseconds - Approximate time to wait, in milliseconds. 1546 | */ 1547 | exports.wait = function(milliseconds) { 1548 | debug('.wait()', milliseconds); 1549 | return this.ready.then(function() { 1550 | return HorsemanPromise.delay(milliseconds); 1551 | }); 1552 | }; 1553 | 1554 | /** 1555 | * Wait for a page load to occur 1556 | * @throws Horseman.TimeoutError 1557 | * @param {object} options 1558 | * @param {number} [options.timeout=undefined] - timeout options 1559 | * @emits Horseman#timeout 1560 | */ 1561 | exports.waitForNextPage = function(options) { 1562 | debug('.waitForNextPage()'); 1563 | var self = this; 1564 | var startCnt = self.lastActionPageCnt; 1565 | var timeout; 1566 | if(typeof(options) !== "undefined" && typeof(options.timeout) === "number"){ 1567 | timeout = options.timeout; 1568 | } else { 1569 | timeout = self.options.timeout; 1570 | } 1571 | return this.ready.then(function() { 1572 | var start = Date.now(); 1573 | return new HorsemanPromise(function(resolve, reject) { 1574 | var waiting = setInterval(function() { 1575 | if (self.pageCnt > startCnt) { 1576 | debug('.waitForNextPage() completed successfully'); 1577 | clearInterval(waiting); 1578 | resolve(); 1579 | } else { 1580 | var diff = Date.now() - start; 1581 | if (diff > timeout) { 1582 | clearInterval(waiting); 1583 | debug('.waitForNextPage() timed out'); 1584 | if (typeof self.page.onTimeout === 'function') { 1585 | self.page.onTimeout('waitForNextPage'); 1586 | } 1587 | reject(new TimeoutError( 1588 | 'timeout duing .waitForNextPage() after ' + 1589 | diff + ' ms')); 1590 | } 1591 | } 1592 | }, self.options.interval); 1593 | }); 1594 | }); 1595 | }; 1596 | 1597 | /** 1598 | * Wait for a selector to be present on the current page 1599 | * @param {string} - The selector on which to wait. 1600 | * @param {object} options 1601 | * @param {number} [options.timeout=undefined] - timeout options 1602 | * @throws Horseman.TimeoutError 1603 | * @emits Horseman#timeout 1604 | */ 1605 | exports.waitForSelector = function(selector, options) { 1606 | debug('.waitForSelector()', selector, options); 1607 | var elementPresent = function elementPresent(selector) { 1608 | var els = window.jQuery ? 1609 | jQuery(selector) : document.querySelectorAll(selector); 1610 | return els.length > 0; 1611 | }; 1612 | 1613 | var opts = { 1614 | fn : elementPresent, 1615 | args : [selector], 1616 | value : true 1617 | }; 1618 | 1619 | if(typeof(options) !== "undefined" && typeof(options.timeout) === "number"){ 1620 | opts.timeout = options.timeout; 1621 | } 1622 | 1623 | return this.waitFor(opts).then(function() { 1624 | debug('.waitForSelector() complete'); 1625 | }); 1626 | }; 1627 | 1628 | /** 1629 | * Waits for a function to evaluate to a given value in browser 1630 | * @param {function | object} optsOrFn - If optsOrFn is a function, use the classic signature waitFor(fn, arg1, arg2, value), If arg is an object, use waitFor(options) 1631 | * @param {array} optsOrFn.args - arguments of fn 1632 | * @param {function} optsOrFn.fn - function to evaluate 1633 | * @param {*} optsOrFn.value - expected value of function 1634 | * @param {number} [optsOrFn.timeout=null] - timeout in ms 1635 | * @param {...*} [arguments] 1636 | * @param {*} value 1637 | * @throws Horseman.TimeoutError 1638 | * @emits Horseman#timeout 1639 | */ 1640 | exports.waitFor = function() { 1641 | var args = Array.prototype.concat.apply([undefined], arguments); 1642 | return waitForPage.apply(this, args); 1643 | }; 1644 | /** 1645 | * Waits for a function to evaluate to a given value on the given page 1646 | * @param {Page | undefined} page 1647 | * @param {function | object} optsOrFn - If optsOrFn is a function, use the classic signature waitForPage(page, fn, arg1, arg2, value), If arg is an object, use waitForPage(page, options) 1648 | * @param {array} optsOrFn.args - arguments of fn 1649 | * @param {function} optsOrFn.fn - function to evaluate 1650 | * @param {*} optsOrFn.value - expected value of function 1651 | * @param {number} [optsOrFn.timeout=null] - timeout in ms 1652 | * @param {...*} [arguments] 1653 | * @param {*} value 1654 | * @emits Horseman#TimeoutError 1655 | */ 1656 | function waitForPage(page, optsOrFn) { 1657 | var self = this; 1658 | var args, value, fname, timeout = self.options.timeout, fn; 1659 | 1660 | if(typeof optsOrFn === "function"){ 1661 | fn = optsOrFn; 1662 | args = Array.prototype.slice.call(arguments); 1663 | value = args.pop(); 1664 | fname = fn.name || ''; 1665 | } else if(typeof optsOrFn === "object"){ 1666 | fn = optsOrFn.fn; 1667 | args = [page, fn].concat(optsOrFn.args || []); 1668 | value = optsOrFn.value; 1669 | fname = fn.name || ''; 1670 | if(optsOrFn.timeout){ 1671 | timeout = optsOrFn.timeout; 1672 | } 1673 | } 1674 | 1675 | debug.apply(debug, ['.waitFor()', fname].concat(args.slice(2))); 1676 | return this.ready.then(function() { 1677 | return new HorsemanPromise(function(resolve, reject) { 1678 | var start = Date.now(); 1679 | var checkInterval = setInterval(function waitForCheck() { 1680 | var _page = page || self.page; 1681 | var diff = Date.now() - start; 1682 | if (diff > timeout) { 1683 | clearInterval(checkInterval); 1684 | debug('.waitFor() timed out'); 1685 | if (typeof _page.onTimeout === 'function') { 1686 | _page.onTimeout('waitFor'); 1687 | } 1688 | reject(new TimeoutError( 1689 | 'timeout during .waitFor() after ' + diff + ' ms')); 1690 | } else { 1691 | return evaluatePage.apply(self, args) 1692 | .tap(function(res) { 1693 | debugv('.waitFor() iteration', 1694 | fname, 1695 | res, 1696 | diff, 1697 | self.id 1698 | ); 1699 | }) 1700 | .then(function(res) { 1701 | if (res === value) { 1702 | debug('.waitFor() completed successfully'); 1703 | clearInterval(checkInterval); 1704 | resolve(); 1705 | } 1706 | }) 1707 | .catch(function(err) { 1708 | clearInterval(checkInterval); 1709 | reject(err); 1710 | }); 1711 | } 1712 | }, self.options.interval); 1713 | }); 1714 | }); 1715 | } 1716 | 1717 | //**************************************************// 1718 | // Configuration 1719 | //**************************************************// 1720 | 1721 | /** 1722 | * Change the proxy settings. 1723 | * @param {string} ip - IP of proxy, or a URI (e.g. proto://user:pass@ip:port) 1724 | * @param {integer} [port=80] - Port of proxy, override URI 1725 | * @param {string} [type=http] - Type of proxy, overrides URI 1726 | * @param {string} [user] - Proxy auth username, overrides URI 1727 | * @param {string} [pass] - Proxy auth password, overrides URI 1728 | */ 1729 | exports.setProxy = function(ip, port, type, user, pass) { 1730 | var self = this; 1731 | return this.ready.then(function() { 1732 | if (ip) { 1733 | // Handle URI (e.g. "protocol://user:pass@ip:port") 1734 | var parsed = url.parse(ip); 1735 | if (!parsed.slashes) { // No protocol was supplied 1736 | parsed = url.parse('http://' + ip); 1737 | } 1738 | ip = parsed.hostname; 1739 | port = port || parseInt(parsed.port) || 80; 1740 | type = type || parsed.protocol.replace(/:$/, ''); 1741 | if (parsed.auth) { 1742 | var auth = parsed.auth.split(':'); 1743 | user = user || auth[0]; 1744 | pass = pass || auth[1]; 1745 | } 1746 | user = user || ''; 1747 | pass = pass || ''; 1748 | } 1749 | 1750 | debug('setProxy', ip, port, type, user, pass); 1751 | return HorsemanPromise.fromCallback(function(done) { 1752 | return self.phantom.setProxy(ip, port, type, user, pass, done); 1753 | }); 1754 | }); 1755 | }; 1756 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clone = require('clone'); 4 | var defaults = require('defaults'); 5 | var phantom = require('node-phantom-simple'); 6 | var path = require('path'); 7 | var debug = require('debug')('horseman'); 8 | var debugv = require('debug')('horseman:verbose'); 9 | var actions = require('./actions'); 10 | var HorsemanPromise = require('./HorsemanPromise.js'); 11 | var noop = function() {}; 12 | 13 | /** 14 | * Check for npm PhantomJS 15 | */ 16 | var phantomjs; 17 | try { 18 | phantomjs = require('phantomjs-prebuilt'); 19 | debug('using PhantomJS from phantomjs-prebuilt module'); 20 | } catch (err) { 21 | try { 22 | phantomjs = require('phantomjs'); 23 | debug('using PhantomJS from phantomjs module'); 24 | } catch (err) { 25 | phantomjs = {}; 26 | debug('using PhantomJS from $PATH'); 27 | } 28 | } 29 | 30 | /** 31 | * Default options. 32 | */ 33 | var DEFAULTS = { 34 | timeout: 5000, 35 | interval: 50, 36 | weak: true, 37 | loadImages: true, 38 | sslProtocol: 'any', //sslv3, sslv2, tlsv1, any 39 | injectJquery: true, 40 | switchToNewTab: false, 41 | injectBluebird: false, 42 | phantomPath: phantomjs.path 43 | }; 44 | 45 | /** 46 | * Give each instance a unique ID for debug purposes 47 | */ 48 | var instanceId = 0; 49 | 50 | 51 | 52 | function prepare(horseman, instantiationOptions) { 53 | return new HorsemanPromise(function(resolve, reject) { 54 | phantom.create(instantiationOptions, function(err, instance) { 55 | debug('phantom created'); 56 | horseman.phantom = instance; 57 | if (err) { 58 | return reject(err); 59 | } 60 | if (debugv.enabled) { 61 | horseman.phantom.onError = function(msg, trace) { 62 | var str = msg; 63 | trace.forEach(function(t) { 64 | var fun = t.function || ''; 65 | str += '\n\t' + t.file + ': ' + t.line + ' in ' + fun; 66 | }); 67 | return debugv('onPhantomError', str, horseman.id); 68 | }; 69 | } 70 | instance.get('version', function(err, ver) { 71 | if (err) { return reject(err); } 72 | var version = ver.major + '.' + ver.minor + '.' + ver.patch; 73 | debug('phantom version ' + version); 74 | return horseman.pageMaker() 75 | .then(resolve) 76 | .catch(reject); 77 | }); 78 | }); 79 | }) 80 | .then(function() { 81 | horseman.pageCnt = 0; 82 | }) 83 | .bind(horseman); 84 | } 85 | 86 | /** 87 | * Creates a new Horseman. 88 | * @constructor 89 | */ 90 | var Horseman = function Horseman(options) { 91 | this.ready = false; 92 | if (!(this instanceof Horseman)) { return new Horseman(options); } 93 | this.options = defaults(clone(options) || {}, DEFAULTS); 94 | 95 | this.id = ++instanceId; 96 | debug('.setup() creating phantom instance %s', this.id); 97 | 98 | var phantomOptions = { 99 | 'load-images': this.options.loadImages, 100 | 'ssl-protocol': this.options.sslProtocol 101 | }; 102 | 103 | if (typeof this.options.ignoreSSLErrors !== 'undefined') { 104 | phantomOptions['ignore-ssl-errors'] = this.options.ignoreSSLErrors; 105 | } 106 | if (typeof this.options.webSecurity !== 'undefined') { 107 | phantomOptions['web-security'] = this.options.webSecurity; 108 | } 109 | if (typeof this.options.proxy !== 'undefined') { 110 | phantomOptions.proxy = this.options.proxy; 111 | } 112 | if (typeof this.options.proxyType !== 'undefined') { 113 | phantomOptions['proxy-type'] = this.options.proxyType; 114 | } 115 | if (typeof this.options.proxyAuth !== 'undefined') { 116 | phantomOptions['proxy-auth'] = this.options.proxyAuth; 117 | } 118 | if (typeof this.options.diskCache !== 'undefined') { 119 | phantomOptions['disk-cache'] = this.options.diskCache; 120 | } 121 | if (typeof this.options.diskCachePath !== 'undefined') { 122 | phantomOptions['disk-cache-path'] = this.options.diskCachePath; 123 | } 124 | if (typeof this.options.cookiesFile !== 'undefined') { 125 | phantomOptions['cookies-file'] = this.options.cookiesFile; 126 | } 127 | 128 | if (this.options.debugPort) { 129 | phantomOptions['remote-debugger-port'] = this.options.debugPort; 130 | phantomOptions['remote-debugger-autorun'] = 'no'; 131 | if (this.options.debugAutorun !== false) { 132 | phantomOptions['remote-debugger-autorun'] = 'yes'; 133 | } 134 | } 135 | 136 | Object.keys(this.options.phantomOptions || {}).forEach(function (key) { 137 | if (typeof phantomOptions[key] !== 'undefined') { 138 | debug('Horseman option ' + key + ' overridden by phantomOptions'); 139 | } 140 | phantomOptions[key] = this.options.phantomOptions[key]; 141 | }.bind(this)); 142 | 143 | var instantiationOptions = { 144 | parameters: phantomOptions 145 | }; 146 | 147 | if (typeof this.options.phantomPath !== 'undefined') { 148 | instantiationOptions['path'] = this.options.phantomPath; 149 | } 150 | 151 | // Store the url that was requested for the current url 152 | this.targetUrl = null; 153 | 154 | // Store the HTTP status code for resources requested. 155 | this.responses = {}; 156 | 157 | this.tabs = []; 158 | this.onTabCreated = noop; 159 | this.onTabClosed = noop; 160 | 161 | this.ready = prepare(this, instantiationOptions); 162 | }; 163 | 164 | /** 165 | * Attaches a page to the Horseman instance. 166 | * @param {string} [url=about:blank] - URL to open. 167 | * @param {Page} [_page] - Page to attach. If null, a new Page will be created. 168 | * @emits Horseman#tabCreated 169 | */ 170 | Horseman.prototype.pageMaker = function(url, _page) { 171 | var self = this; 172 | url = url || 'about:blank'; 173 | 174 | // .try runs synchronously 175 | var loaded; 176 | return HorsemanPromise.try(function getPage() { 177 | var page; 178 | if (_page) { 179 | var p = setupPage(_page); 180 | loaded = _page.loadedPromise(); 181 | return p; 182 | } else { 183 | return HorsemanPromise.fromCallback(function(done) { 184 | return self.phantom.createPage(function(err, page) { 185 | if (page) { 186 | var p = setupPage(page).asCallback(done); 187 | loaded = page.loadedPromise(); 188 | return p; 189 | } 190 | return done(err, page); 191 | }); 192 | }) 193 | .tap(function openPage(page) { 194 | return HorsemanPromise.fromCallback(function(done) { 195 | return page.open(url, done); 196 | }); 197 | }); 198 | } 199 | }) 200 | .tap(function waitForLoadFinished() { 201 | return loaded; 202 | }) 203 | .tap(function attachPage(page) { 204 | if (!_page || self.options.switchToNewTab) { 205 | self.page = page; 206 | } 207 | var tabNum = self.tabs.push(page) - 1; 208 | return self.onTabCreated(tabNum, page); 209 | }) 210 | .bind(self); 211 | 212 | /** 213 | * Needs to be called *BEFORE* page creation callback returns 214 | * This is necessary to avoid having events before callbacks are attached 215 | * @param page - Newly created page to decorate and attach to horseman 216 | */ 217 | function setupPage(page) { 218 | debug('page created'); 219 | 220 | page.onPageCreated = function(newPage) { 221 | return self.ready = self.pageMaker(undefined, newPage) 222 | .tap(function() { 223 | page.onPageCreated2(newPage); 224 | }); 225 | }; 226 | page.onPageCreated2 = noop; 227 | 228 | page.onResourceReceived = function(response) { 229 | self.responses[response.url] = response.status; 230 | }; 231 | 232 | page.onLoadFinished = loadFinishedSetup; 233 | 234 | page.onClosing = function() { 235 | var tabNum = self.tabs.indexOf(page); 236 | debug('closing tab', tabNum); 237 | self.tabs.splice(tabNum, 1); 238 | if (self.page === page) { 239 | // Switch to previous tab when current tab closes 240 | self.page = self.tabs[tabNum - 1]; 241 | } 242 | self.onTabClosed(tabNum, page); 243 | }; 244 | 245 | page.onTimeout = noop; 246 | 247 | // Create a Promise for when onLoadFinished callback is done 248 | page.loadedPromise = function() { 249 | return HorsemanPromise.fromCallback(function(done) { 250 | page.loadDone = function(err, res) { 251 | page.loadDone = undefined; 252 | return done(err, res); 253 | }; 254 | }); 255 | }; 256 | 257 | // If verbose debug, add default error and consoleMessage handlers 258 | if (debugv.enabled) { 259 | page.onError = function(msg, trace) { 260 | var str = msg; 261 | trace.forEach(function(t) { 262 | var fun = t.function || ''; 263 | str += '\n\t' + t.file + ': ' + t.line + ' in ' + fun; 264 | }); 265 | return debugv('onError', str, self.id); 266 | }; 267 | page.onConsoleMessage = function(msg, lineNum, sourceId) { 268 | var str = msg + ' line: ' + lineNum + ' in ' + sourceId; 269 | return debugv('onConsoleMessage', str, self.id); 270 | }; 271 | } 272 | 273 | return HorsemanPromise.fromCallback(function(done) { 274 | return page.set( 275 | 'settings.resourceTimeout', 276 | self.options.timeout, 277 | done 278 | ); 279 | }).return(page); 280 | 281 | /** 282 | * Do any javascript injection on the page 283 | * TODO: Consolidate into one page.evaluate? 284 | */ 285 | function loadFinishedSetup(status) { 286 | var args = arguments; 287 | self.pageCnt++; 288 | debug('phantomjs onLoadFinished triggered', status, self.pageCnt); 289 | 290 | return self.ready = HorsemanPromise.try(function checkStatus() { 291 | if (status !== 'success') { 292 | var err = new Error('Failed to load url'); 293 | return HorsemanPromise.reject(err); 294 | } 295 | }) 296 | .then(function injectJQuery() { 297 | if (!self.options.injectJquery) { 298 | return; 299 | } 300 | 301 | return HorsemanPromise.fromCallback(function hasJQuery(done) { 302 | return page.evaluate(function hasJQuery() { 303 | return (typeof window.jQuery !== 'undefined'); 304 | }, done); 305 | }) 306 | .then(function(hasJquery) { 307 | if (hasJquery) { 308 | debug('jQuery not injected - already exists on page'); 309 | return; 310 | } 311 | 312 | var jQueryLocation = path.join(__dirname, 313 | '../files/jquery-2.1.1.min.js'); 314 | return HorsemanPromise.fromCallback(function(done) { 315 | return page.injectJs(jQueryLocation, done); 316 | }) 317 | .tap(function(successful) { 318 | if (!successful) { 319 | var err = new Error('jQuery injection failed'); 320 | return HorsemanPromise.reject(err); 321 | } 322 | debug('injected jQuery'); 323 | }); 324 | }); 325 | }) 326 | .then(function injectBluebird() { 327 | var inject = self.options.injectBluebird; 328 | if (!inject) { 329 | return; 330 | } 331 | 332 | return HorsemanPromise.fromCallback(function hasPromise(done) { 333 | return page.evaluate(function hasPromise() { 334 | return (typeof window.Promise !== 'undefined'); 335 | }, done); 336 | }) 337 | .then(function(hasPromise) { 338 | if (hasPromise && inject !== 'bluebird') { 339 | debug('bluebird not injected - ' + 340 | 'Promise already exists on page'); 341 | return; 342 | } 343 | 344 | var bbLoc = 'bluebird/js/browser/bluebird' + 345 | (self.options.bluebirdDebug ? '' : '.min') + '.js'; 346 | return HorsemanPromise.fromCallback(function(done) { 347 | return page.injectJs(require.resolve(bbLoc), done); 348 | }) 349 | .tap(function(successful) { 350 | if (!successful) { 351 | var err = new Error('bluebird injection failed'); 352 | return HorsemanPromise.reject(err); 353 | } 354 | debug('injected bluebird'); 355 | }); 356 | 357 | }) 358 | .then(function configBluebird() { 359 | return HorsemanPromise.fromCallback(function(done) { 360 | return page.evaluate( 361 | function configBluebird(noConflict, debug) { 362 | if (debug) { 363 | // TODO: Turn on warnings in bluebird 3 364 | Promise.longStackTraces(); 365 | } 366 | if (noConflict) { 367 | window.Bluebird = Promise.noConflict(); 368 | } 369 | }, 370 | inject === 'bluebird', 371 | self.options.bluebirdDebug, 372 | done); 373 | }); 374 | }); 375 | }) 376 | .then(function initWindow() { 377 | return HorsemanPromise.fromCallback(function(done) { 378 | return page.evaluate(function initWindow() { 379 | window.__horseman = {}; 380 | }, done); 381 | }); 382 | }) 383 | .then(function finishLoad() { 384 | if (page.onLoadFinished2) { 385 | return page.onLoadFinished2.apply(page, args); 386 | } 387 | }) 388 | .bind(self) 389 | .asCallback(page.loadDone); 390 | } 391 | } 392 | }; 393 | 394 | //**************************************************// 395 | // Actions 396 | //**************************************************// 397 | /** 398 | * Add an action to the Horseman (and HorsemanPromise) prototype. 399 | * @param {string} name - Name to access action with 400 | * @param {function} action - Code for the action. 401 | * this will be bound to the horseman instance. 402 | */ 403 | Horseman.registerAction = function registerAction(name, action) { 404 | if (typeof name === 'function') { 405 | // Work with just a named function 406 | action = name; 407 | name = action.name; 408 | } 409 | 410 | // Keep track of page counter before each action 411 | Horseman.prototype[name] = function() { 412 | this.lastActionPageCnt = this.curActionPageCnt; 413 | this.curActionPageCnt = this.pageCnt; 414 | return action.apply(this, arguments); 415 | }; 416 | 417 | // Allow chaining actions off HorsemanPromises 418 | HorsemanPromise.prototype[name] = function() { 419 | var args = arguments; 420 | return this.then(function(val) { 421 | this.lastVal = val; 422 | return this[name].apply(this, args); 423 | }); 424 | }; 425 | }; 426 | 427 | /** 428 | * Attach all of the actions. 429 | */ 430 | Object.keys(actions).forEach(function attachAction(name) { 431 | Horseman.registerAction(name, actions[name]); 432 | }); 433 | 434 | //**************************************************// 435 | // Cleanup 436 | //**************************************************// 437 | Horseman.prototype.close = function() { 438 | debug('.close().'); 439 | var self = this; 440 | return this.ready.finally(function() { 441 | var p; 442 | if (!self.closed && self.phantom) { 443 | p = HorsemanPromise.fromCallback(function(done) { 444 | return self.phantom.exit(done); 445 | }); 446 | } 447 | //self.ready = HorsemanPromise.reject(new Error('phantom is closed')); 448 | self.closed = true; 449 | return p || HorsemanPromise.resolve(); 450 | }); 451 | }; 452 | HorsemanPromise.prototype.close = function() { 453 | var self = this; 454 | return this 455 | .finally(function() { 456 | return this.close(); 457 | }).finally(function() { 458 | return self; 459 | }); 460 | }; 461 | 462 | module.exports = Horseman; 463 | module.exports.TimeoutError = HorsemanPromise.TimeoutError; 464 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-horseman", 3 | "version": "3.3.0", 4 | "description": "Run PhantomJS from Node", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/johntitus/node-horseman.git" 8 | }, 9 | "main": "lib/index", 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "dependencies": { 14 | "bluebird": "^3.0.1", 15 | "clone": "^1.0.2", 16 | "cookies.txt": "^0.1.1", 17 | "data-uri-to-buffer": "0.0.4", 18 | "debug": "^2.1.1", 19 | "defaults": "~1.0.0", 20 | "node-phantom-simple": "^2.2.4" 21 | }, 22 | "devDependencies": { 23 | "express": "^4.10.4", 24 | "hoxy": "^3.2.0", 25 | "mocha": "^2.2.5", 26 | "mocha.parallel": "^0.12.0", 27 | "request": "^2.69.0", 28 | "rmdir": "^1.2.0", 29 | "semver": "^5.1.0", 30 | "should": "^8.2.2" 31 | }, 32 | "scripts": { 33 | "lint": "jshint .", 34 | "test": "mocha -R spec" 35 | }, 36 | "keywords": [ 37 | "phantomjs", 38 | "horseman", 39 | "headless", 40 | "browser" 41 | ], 42 | "author": "John Titus ", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /test/files/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | background-color: white; 3 | } 4 | a { 5 | margin-bottom: 3px; 6 | } -------------------------------------------------------------------------------- /test/files/cookies.txt: -------------------------------------------------------------------------------- 1 | # Dummy cookies.txt for horseman tests 2 | .httpbin.org TRUE / FALSE 99999999 test cookie 3 | .httpbin.org TRUE / FALSE 99999999 test2 cookie2 4 | -------------------------------------------------------------------------------- /test/files/frame1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman frame 1 6 | 7 | 8 |

This is frame 1.

9 | 10 | -------------------------------------------------------------------------------- /test/files/frame2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman frame 2 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/files/frame3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman frame 3 6 | 7 | 8 |

This is frame 3.

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/files/frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman test frames 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Page 5 | 6 | 7 | 8 | 9 | 10 |

11 | This is my code. 12 |

13 | 14 | 18 | 19 | Next Page 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/files/jQuery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman fake jQuery page 6 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/files/newtab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman new tab 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/files/next.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Next Page 5 | 6 | 7 | 8 | 9 | 10 | Root 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/files/opennewtab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Horseman test link open new tab 6 | 7 | Open new tab 8 | 9 | -------------------------------------------------------------------------------- /test/files/plainText.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Plain Page 6 | 7 | 8 |

This is some plain text.

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/files/test.txt: -------------------------------------------------------------------------------- 1 | This is test text. 2 | -------------------------------------------------------------------------------- /test/files/testcss.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | background-color: #FF0000; 3 | } -------------------------------------------------------------------------------- /test/files/testjs.js: -------------------------------------------------------------------------------- 1 | var ___obj = { 2 | myname: 'isbob' 3 | }; 4 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Horseman = require('../lib'); 4 | var actions = require('../lib/actions'); 5 | var fs = require('fs'); 6 | var os = require('os'); 7 | var path = require('path'); 8 | var Promise = require('bluebird'); 9 | var express = require('express'); 10 | var semver = require('semver'); 11 | var hoxy = require('hoxy'); 12 | var request = require('request'); 13 | var should = require('should'); 14 | var parallel = require('mocha.parallel'); 15 | var rmdir = require('rmdir'); 16 | 17 | var app; 18 | var server; 19 | var serverUrl; 20 | var hostname = 'http://localhost'; 21 | var defaultPort = 4567; 22 | var defaultTimeout = 8000; // Increase timeout for Travis 23 | 24 | function navigation(bool) { 25 | var title = 'Navigation ' + ((bool) ? 'with' : 'without') + ' jQuery'; 26 | 27 | parallel(title, function() { 28 | it('should set the user agent', function() { 29 | var horseman = new Horseman({ 30 | timeout: defaultTimeout, 31 | injectJquery: bool 32 | }); 33 | var userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' + 34 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 35 | 'Chrome/37.0.2062.124 Safari/537.36'; 36 | return horseman 37 | .userAgent(userAgent) 38 | .open(serverUrl) 39 | .evaluate(function() { 40 | return navigator.userAgent; 41 | }) 42 | .close() 43 | .should.eventually 44 | .equal(userAgent); 45 | 46 | }); 47 | 48 | it('should set headers', function() { 49 | var headers = { 50 | 'X-Horseman-Header': 'test header' 51 | }; 52 | var horseman = new Horseman({ 53 | timeout: defaultTimeout, 54 | injectJquery: bool 55 | }); 56 | return horseman 57 | .headers(headers) 58 | .open('http://httpbin.org/headers') 59 | .evaluate(function() { 60 | return document.body.children[0].innerHTML; 61 | }) 62 | .close() 63 | .then(JSON.parse) 64 | .should.eventually 65 | .have.property('headers') 66 | .with.property('X-Horseman-Header', 'test header'); 67 | }); 68 | 69 | it('should open a page', function() { 70 | var horseman = new Horseman({ 71 | timeout: defaultTimeout, 72 | injectJquery: bool 73 | }); 74 | return horseman 75 | .open(serverUrl) 76 | .url() 77 | .close() 78 | .should.eventually 79 | .equal(serverUrl); 80 | }); 81 | 82 | it('should reject on fail', function() { 83 | var port = (process.env.port || defaultPort) + 1; 84 | var requestUrl = hostname + ':' + port + '/'; 85 | 86 | var horseman = new Horseman({ 87 | timeout: defaultTimeout, 88 | injectJquery: bool 89 | }); 90 | return horseman 91 | .open(requestUrl) 92 | .close() 93 | .should 94 | .be.rejectedWith({ 95 | message: 'Failed to GET url: ' + requestUrl 96 | }); 97 | }); 98 | 99 | it('should have a HTTP status code', function() { 100 | var horseman = new Horseman({ 101 | timeout: defaultTimeout, 102 | injectJquery: bool 103 | }); 104 | return horseman 105 | .open(serverUrl) 106 | .status() 107 | .close() 108 | .should.eventually 109 | .equal(200); 110 | }); 111 | 112 | it('should click a link', function() { 113 | var horseman = new Horseman({ 114 | timeout: defaultTimeout, 115 | injectJquery: bool 116 | }); 117 | return horseman 118 | .open(serverUrl) 119 | .click('a[href="next.html"]') 120 | .waitForNextPage() 121 | .url() 122 | .close() 123 | .should.eventually 124 | .equal(serverUrl + 'next.html'); 125 | }); 126 | 127 | it('should go backwards and forwards', function() { 128 | var horseman = new Horseman({ 129 | timeout: defaultTimeout, 130 | injectJquery: bool 131 | }); 132 | return horseman 133 | .open(serverUrl) 134 | .click('a[href="next.html"]') 135 | .waitForNextPage() 136 | .back() 137 | .waitForNextPage() 138 | .url() 139 | .then(function(data) { 140 | data.should.equal(serverUrl); 141 | }) 142 | .forward() 143 | .waitForNextPage() 144 | .url() 145 | .close() 146 | .should.eventually 147 | .equal(serverUrl + 'next.html'); 148 | }); 149 | 150 | it('should use basic authentication', function() { 151 | var horseman = new Horseman({ 152 | timeout: defaultTimeout, 153 | injectJquery: bool 154 | }); 155 | return horseman 156 | .authentication('my', 'auth') 157 | .open('http://httpbin.org/basic-auth/my/auth') 158 | .evaluate(function() { 159 | return document.body.innerHTML.length; 160 | }) 161 | .close() 162 | .should.eventually 163 | .be.above(0); 164 | }); 165 | 166 | it('should pass custom options to Phantom', function() { 167 | var horseman = new Horseman({ 168 | timeout: defaultTimeout, 169 | injectJquery: bool, 170 | ignoreSSLErrors: true, 171 | phantomOptions: { 172 | 'ignore-ssl-errors': false 173 | } 174 | }); 175 | return horseman 176 | .open('https://expired.badssl.com') 177 | .close() 178 | .should 179 | .be.rejected(); 180 | }); 181 | 182 | it('should set the viewport', function() { 183 | var horseman = new Horseman({ 184 | timeout: defaultTimeout, 185 | injectJquery: bool 186 | }); 187 | var size = { 188 | width: 400, 189 | height: 1000 190 | }; 191 | return horseman 192 | .viewport(size.width, size.height) 193 | .open('http://www.google.com') 194 | .viewport() 195 | .close() 196 | .should.eventually 197 | .have.properties(size); 198 | }); 199 | 200 | it('should let you scroll', function() { 201 | var horseman = new Horseman({ 202 | timeout: defaultTimeout, 203 | injectJquery: bool 204 | }); 205 | return horseman 206 | .open('http://www.google.com') 207 | .scrollTo(50, 40) 208 | .evaluate(function() { 209 | return { 210 | top: document.body.scrollTop, 211 | left: document.body.scrollLeft 212 | }; 213 | }) 214 | .close() 215 | .should.eventually 216 | .have.properties({ 217 | top: 50, 218 | left: 40 219 | }); 220 | }); 221 | 222 | it('should add a cookie', function() { 223 | var horseman = new Horseman({ 224 | timeout: defaultTimeout, 225 | injectJquery: bool 226 | }); 227 | var cookie = { 228 | name: 'test', 229 | value: 'cookie', 230 | domain: 'httpbin.org' 231 | }; 232 | 233 | return horseman 234 | .cookies(cookie) 235 | .open('http://httpbin.org/cookies') 236 | .text('pre') 237 | .close() 238 | .then(JSON.parse) 239 | .get('cookies') 240 | .should.eventually 241 | .have.property(cookie.name, cookie.value); 242 | }); 243 | 244 | it('should clear out all cookies', function() { 245 | var horseman = new Horseman({ 246 | timeout: defaultTimeout, 247 | injectJquery: bool 248 | }); 249 | return horseman 250 | .cookies([]) 251 | .open('http://httpbin.org/cookies') 252 | .text('pre') 253 | .close() 254 | .then(JSON.parse) 255 | .should.eventually 256 | .not.have.keys(); 257 | }); 258 | 259 | it('should add an array of cookies', function() { 260 | var horseman = new Horseman({ 261 | timeout: defaultTimeout, 262 | injectJquery: bool 263 | }); 264 | var cookies = [{ 265 | name: 'test', 266 | value: 'cookie', 267 | domain: 'httpbin.org' 268 | }, { 269 | name: 'test2', 270 | value: 'cookie2', 271 | domain: 'httpbin.org' 272 | }]; 273 | 274 | return horseman 275 | .cookies(cookies) 276 | .open('http://httpbin.org/cookies') 277 | .text('pre') 278 | .close() 279 | .then(JSON.parse) 280 | .get('cookies') 281 | .then(function(result) { 282 | return cookies.forEach(function(cookie) { 283 | result.should.have.property(cookie.name, cookie.value); 284 | }); 285 | }); 286 | }); 287 | 288 | it('should add cookies.txt file', function() { 289 | var horseman = new Horseman({ 290 | timeout: defaultTimeout, 291 | injectJquery: bool 292 | }); 293 | // Must keep these up to date with files/cookies.txt 294 | var cookies = [{ 295 | name: 'test', 296 | value: 'cookie', 297 | domain: 'httpbin.org' 298 | }, { 299 | name: 'test2', 300 | value: 'cookie2', 301 | domain: 'httpbin.org' 302 | }]; 303 | var COOKIES_TXT = path.join(__dirname, 'files', 'cookies.txt'); 304 | 305 | return horseman 306 | .cookies(COOKIES_TXT) 307 | .open('http://httpbin.org/cookies') 308 | .text('pre') 309 | .close() 310 | .then(JSON.parse) 311 | .get('cookies') 312 | .then(function(result) { 313 | return cookies.forEach(function(cookie) { 314 | result.should.have.property(cookie.name, cookie.value); 315 | }); 316 | }); 317 | }); 318 | 319 | it('should post to a page', function() { 320 | var horseman = new Horseman({ 321 | timeout: defaultTimeout, 322 | injectJquery: bool 323 | }); 324 | var data = 'universe=expanding&answer=42'; 325 | 326 | return horseman 327 | .post('http://httpbin.org/post', data) 328 | .text('pre') 329 | .close() 330 | .then(JSON.parse) 331 | .should.eventually 332 | .have.property('form') 333 | .with.properties({ 334 | answer: '42', 335 | universe: 'expanding' 336 | }); 337 | }); 338 | 339 | it('should return a status from open', function() { 340 | var horseman = new Horseman({ 341 | timeout: defaultTimeout, 342 | injectJquery: bool 343 | }); 344 | return horseman 345 | .open(serverUrl) 346 | .close() 347 | .should.eventually 348 | .equal('success'); 349 | }); 350 | 351 | }); 352 | } 353 | 354 | function evaluation(bool) { 355 | var title = 'Evaluation ' + ((bool) ? 'with' : 'without') + ' jQuery'; 356 | 357 | parallel(title, function() { 358 | after(function unlinkFiles() { 359 | return Promise 360 | .fromCallback(function(done) { 361 | return fs.stat('test.html', done); 362 | }) 363 | .call('isFile') 364 | .catch(function() {}) 365 | .then(function(isFile) { 366 | if (!isFile) { 367 | return; 368 | } 369 | return Promise.fromCallback(function(done) { 370 | return fs.unlink('test.html', done); 371 | }); 372 | }); 373 | }); 374 | 375 | it('should get the title', function() { 376 | var horseman = new Horseman({ 377 | timeout: defaultTimeout, 378 | injectJquery: bool 379 | }); 380 | return horseman 381 | .open(serverUrl) 382 | .title() 383 | .close() 384 | .should.eventually 385 | .equal('Testing Page'); 386 | }); 387 | 388 | it('should verify an element exists', function() { 389 | var horseman = new Horseman({ 390 | timeout: defaultTimeout, 391 | injectJquery: bool 392 | }); 393 | return horseman 394 | .open(serverUrl) 395 | .exists('input') 396 | .close() 397 | .should.eventually 398 | .be.true(); 399 | }); 400 | 401 | it('should verify an element does not exists', function() { 402 | var horseman = new Horseman({ 403 | timeout: defaultTimeout, 404 | injectJquery: bool 405 | }); 406 | return horseman 407 | .open(serverUrl) 408 | .exists('article') 409 | .close() 410 | .should.eventually 411 | .be.false(); 412 | }); 413 | 414 | it('should count the number of selectors', function() { 415 | var horseman = new Horseman({ 416 | timeout: defaultTimeout, 417 | injectJquery: bool 418 | }); 419 | return horseman 420 | .open(serverUrl) 421 | .count('a') 422 | .close() 423 | .should.eventually 424 | .be.above(0); 425 | }); 426 | 427 | it('should get the html of an element', function() { 428 | var horseman = new Horseman({ 429 | timeout: defaultTimeout, 430 | injectJquery: bool 431 | }); 432 | return horseman 433 | .open(serverUrl) 434 | .html('#text') 435 | .close() 436 | .should.eventually 437 | .match(/code/); 438 | }); 439 | 440 | it('should write the html of an element', function() { 441 | var horseman = new Horseman({ 442 | timeout: defaultTimeout, 443 | injectJquery: bool 444 | }); 445 | return horseman 446 | .open(serverUrl) 447 | .html('', 'test.html') 448 | .close() 449 | .then(function() { 450 | return Promise.fromCallback(function(done) { 451 | return fs.stat('test.html', done); 452 | }); 453 | }) 454 | .call('isFile') 455 | .should.eventually 456 | .be.true(); 457 | }); 458 | 459 | it('should get the text of an element', function() { 460 | var horseman = new Horseman({ 461 | timeout: defaultTimeout, 462 | injectJquery: bool 463 | }); 464 | return horseman 465 | .open(serverUrl) 466 | .text('#text') 467 | .close() 468 | .call('trim') 469 | .should.eventually 470 | .equal('This is my code.'); 471 | }); 472 | 473 | it('should get the plain text of the page', function() { 474 | var horseman = new Horseman({ 475 | timeout: defaultTimeout, 476 | injectJquery: bool 477 | }); 478 | return horseman 479 | .open(serverUrl + "/plainText.html") 480 | .plainText() 481 | .close() 482 | .call('trim') 483 | .should.eventually 484 | .equal("This is some plain text."); 485 | }); 486 | 487 | it('should get the value of an element', function() { 488 | var horseman = new Horseman({ 489 | timeout: defaultTimeout, 490 | injectJquery: bool 491 | }); 492 | return horseman 493 | .open(serverUrl) 494 | .value('input[name="input1"]') 495 | .close() 496 | .should.eventually 497 | .equal(''); 498 | }); 499 | 500 | it('should get an attribute of an element', function() { 501 | var horseman = new Horseman({ 502 | timeout: defaultTimeout, 503 | injectJquery: bool 504 | }); 505 | return horseman 506 | .open(serverUrl) 507 | .attribute('a', 'href') 508 | .close() 509 | .should.eventually 510 | .equal('next.html'); 511 | }); 512 | 513 | it('should get a css property of an element', function() { 514 | var horseman = new Horseman({ 515 | timeout: defaultTimeout, 516 | injectJquery: bool 517 | }); 518 | return horseman 519 | .open(serverUrl) 520 | .cssProperty('a', 'margin-bottom') 521 | .close() 522 | .should.eventually 523 | .equal('3px'); 524 | }); 525 | 526 | it('should get the width of an element', function() { 527 | var horseman = new Horseman({ 528 | timeout: defaultTimeout, 529 | injectJquery: bool 530 | }); 531 | return horseman 532 | .open(serverUrl) 533 | .width('a') 534 | .close() 535 | .should.eventually 536 | .be.above(0); 537 | }); 538 | 539 | it('should get the height of an element', function() { 540 | var horseman = new Horseman({ 541 | timeout: defaultTimeout, 542 | injectJquery: bool 543 | }); 544 | return horseman 545 | .open(serverUrl) 546 | .height('a') 547 | .close() 548 | .should.eventually 549 | .be.above(0); 550 | }); 551 | 552 | it('should determine if an element is visible', function() { 553 | var horseman = new Horseman({ 554 | timeout: defaultTimeout, 555 | injectJquery: bool 556 | }); 557 | return horseman 558 | .open(serverUrl) 559 | .visible('a') 560 | .close() 561 | .should.eventually 562 | .be.true(); 563 | }); 564 | 565 | it('should determine if an element is not-visible', function() { 566 | var horseman = new Horseman({ 567 | timeout: defaultTimeout, 568 | injectJquery: bool 569 | }); 570 | return horseman 571 | .open(serverUrl) 572 | .visible('.login-popup') 573 | .close() 574 | .should.eventually 575 | .be.false(); 576 | }); 577 | 578 | it('should evaluate javascript', function() { 579 | var horseman = new Horseman({ 580 | timeout: defaultTimeout, 581 | injectJquery: bool 582 | }); 583 | return horseman 584 | .open(serverUrl) 585 | .evaluate(function() { 586 | return document.title; 587 | }) 588 | .close() 589 | .should.eventually 590 | .equal('Testing Page'); 591 | }); 592 | 593 | it('should evaluate javascript with optional parameters', function() { 594 | var horseman = new Horseman({ 595 | timeout: defaultTimeout, 596 | injectJquery: bool 597 | }); 598 | var str = 'yo'; 599 | return horseman 600 | .open(serverUrl) 601 | .evaluate(function(param) { 602 | return param; 603 | }, str) 604 | .close() 605 | .should.eventually 606 | .equal(str); 607 | }); 608 | 609 | it('should evaluate Promise/thenable', function() { 610 | var horseman = new Horseman({ 611 | timeout: defaultTimeout, 612 | injectBluebird: true, 613 | injectJquery: bool 614 | }); 615 | var str = 'yo'; 616 | return horseman 617 | .open(serverUrl) 618 | .evaluate(function(param) { 619 | return Promise.resolve(param).delay(10); 620 | }, str) 621 | .close() 622 | .should.eventually 623 | .equal(str); 624 | }); 625 | 626 | it('should evaluate with callback', function() { 627 | var horseman = new Horseman({ 628 | timeout: defaultTimeout, 629 | injectJquery: bool 630 | }); 631 | var str = 'yo'; 632 | return horseman 633 | .open(serverUrl) 634 | .evaluate(function(param, done) { 635 | return setTimeout(function() { 636 | done(null, param); 637 | }, 10); 638 | }, str) 639 | .close() 640 | .should.eventually 641 | .equal(str); 642 | }); 643 | 644 | it('should reject Promise on evaluate throw', function() { 645 | var horseman = new Horseman({ 646 | timeout: defaultTimeout, 647 | injectJquery: bool 648 | }); 649 | return horseman 650 | .open(serverUrl) 651 | .evaluate(function() { 652 | throw new Error(); 653 | }) 654 | .close() 655 | .should 656 | .be.rejected(); 657 | }); 658 | 659 | it('should reject Promise on evaluate reject', function() { 660 | var horseman = new Horseman({ 661 | timeout: defaultTimeout, 662 | injectJquery: bool 663 | }); 664 | return horseman 665 | .open(serverUrl) 666 | .evaluate(function() { 667 | return Promise.reject(new Error()); 668 | }) 669 | .close() 670 | .should 671 | .be.rejected(); 672 | }); 673 | 674 | it('should reject Promise on evaluate callback err', function() { 675 | var horseman = new Horseman({ 676 | timeout: defaultTimeout, 677 | injectJquery: bool 678 | }); 679 | return horseman 680 | .open(serverUrl) 681 | .evaluate(function(done) { 682 | return setTimeout(function() { 683 | done(new Error()); 684 | }, 10); 685 | }) 686 | .close() 687 | .should 688 | .be.rejected(); 689 | }); 690 | 691 | }); 692 | } 693 | 694 | function manipulation(bool) { 695 | var title = 'Manipulation ' + ((bool) ? 'with' : 'without') + ' jQuery'; 696 | 697 | parallel(title, function() { 698 | after(function unlinkFiles() { 699 | var files = [ 700 | 'out.png', 701 | 'small.png', 702 | 'big.png', 703 | 'default.pdf', 704 | 'euro.pdf' 705 | ]; 706 | return Promise.map(files, function(file) { 707 | return Promise 708 | .fromCallback(function(done) { 709 | return fs.stat(file, done); 710 | }) 711 | .call('isFile') 712 | .catch(function() {}) 713 | .then(function(isFile) { 714 | if (!isFile) { 715 | return; 716 | } 717 | return Promise.fromCallback(function(done) { 718 | return fs.unlink(file, done); 719 | }); 720 | }); 721 | }); 722 | }); 723 | 724 | it('should inject javascript', function() { 725 | var horseman = new Horseman({ 726 | timeout: defaultTimeout, 727 | injectJquery: bool 728 | }); 729 | return horseman 730 | .open(serverUrl) 731 | .injectJs('test/files/testjs.js') 732 | .evaluate(function() { 733 | return ___obj.myname; 734 | }) 735 | .close() 736 | .should.eventually 737 | .equal('isbob'); 738 | }); 739 | 740 | it('should reject when inject javascript fails', function() { 741 | var horseman = new Horseman({ 742 | timeout: defaultTimeout, 743 | injectJquery: bool 744 | }); 745 | return horseman 746 | .open(serverUrl) 747 | .injectJs('test/files/not_a_real_file.js') 748 | .close() 749 | .should.be.rejected(); 750 | }); 751 | 752 | it('should include javascript', function() { 753 | var horseman = new Horseman({ 754 | timeout: defaultTimeout, 755 | injectJquery: bool 756 | }); 757 | return horseman 758 | .open(serverUrl) 759 | .includeJs(serverUrl + '/testjs.js') 760 | .evaluate(function() { 761 | return ___obj.myname; 762 | }) 763 | .close() 764 | .should.eventually 765 | .equal('isbob'); 766 | }); 767 | 768 | it('should type and click', function() { 769 | var horseman = new Horseman({ 770 | timeout: defaultTimeout, 771 | injectJquery: bool 772 | }); 773 | return horseman 774 | .open(serverUrl) 775 | .type('input[name="input1"]', 'github') 776 | .value('input[name="input1"]') 777 | .close() 778 | .should.eventually 779 | .equal('github'); 780 | }); 781 | 782 | it('should clear a field', function() { 783 | var horseman = new Horseman({ 784 | timeout: defaultTimeout, 785 | injectJquery: bool 786 | }); 787 | return horseman 788 | .open(serverUrl) 789 | .type('input[name="input1"]', 'github') 790 | .clear('input[name="input1"]') 791 | .value('input[name="input1"]') 792 | .close() 793 | .should.eventually 794 | .equal(''); 795 | }); 796 | 797 | it('should select a value', function() { 798 | var horseman = new Horseman({ 799 | timeout: defaultTimeout, 800 | injectJquery: bool 801 | }); 802 | return horseman 803 | .open(serverUrl) 804 | .select('#select1', '1') 805 | .value('#select1') 806 | .close() 807 | .should.eventually 808 | .equal('1'); 809 | 810 | }); 811 | 812 | it('should take a screenshot', function() { 813 | var horseman = new Horseman({ 814 | timeout: defaultTimeout, 815 | injectJquery: bool 816 | }); 817 | return horseman 818 | .open(serverUrl) 819 | .screenshot('out.png') 820 | .close() 821 | .then(function() { 822 | return Promise.fromCallback(function(done) { 823 | return fs.stat('out.png', done); 824 | }); 825 | }) 826 | .call('isFile') 827 | .should.eventually 828 | .be.true(); 829 | }); 830 | 831 | it('should take a screenshotBase64', function() { 832 | var horseman = new Horseman({ 833 | timeout: defaultTimeout, 834 | injectJquery: bool 835 | }); 836 | return horseman 837 | .open(serverUrl) 838 | .screenshotBase64('PNG') 839 | .close() 840 | .should.eventually 841 | .be.a.String(); 842 | }); 843 | 844 | it('should take a cropBase64', function() { 845 | var horseman = new Horseman({ 846 | timeout: defaultTimeout, 847 | injectJquery: bool 848 | }); 849 | return horseman 850 | .open(serverUrl) 851 | .cropBase64( 852 | {top: 100, left: 100, width: 100, height: 100}, 853 | 'PNG' 854 | ) 855 | .close() 856 | .should.eventually 857 | .be.a.String(); 858 | }); 859 | 860 | it('should let you zoom', function() { 861 | var horseman = new Horseman({ 862 | timeout: defaultTimeout, 863 | injectJquery: bool 864 | }); 865 | return horseman 866 | .open(serverUrl) 867 | .viewport(800, 400) 868 | .open(serverUrl) 869 | .screenshot('small.png') 870 | .viewport(1600, 800) 871 | .zoom(2) 872 | .open(serverUrl) 873 | .screenshot('big.png') 874 | .close() 875 | .then(function() { 876 | var small = Promise.fromCallback(function(done) { 877 | return fs.stat('small.png', done); 878 | }).get('size'); 879 | var big = Promise.fromCallback(function(done) { 880 | return fs.stat('big.png', done); 881 | }).get('size'); 882 | return Promise.join(small, big, function(small, big) { 883 | big.should.be.greaterThan(small); 884 | }); 885 | }); 886 | }); 887 | 888 | it('should let you export as a pdf', function() { 889 | var horseman = new Horseman({ 890 | timeout: defaultTimeout, 891 | injectJquery: bool 892 | }); 893 | horseman 894 | .open('http://www.google.com') 895 | .pdf('default.pdf') 896 | .pdf('euro.pdf', { 897 | format: 'A5', 898 | orientation: 'portrait', 899 | margin: 0 900 | }) 901 | .close() 902 | .then(function() { 903 | var defaultSize = Promise.fromCallback(function(done) { 904 | return fs.stat('default.pdf', done); 905 | }).get('size'); 906 | var euroSize = Promise.fromCallback(function(done) { 907 | return fs.stat('euro.pdf', done); 908 | }).get('size'); 909 | return Promise.all([ 910 | defaultSize.should.eventually.be.greaterThan(0), 911 | euroSize.should.eventually.be.greaterThan(0) 912 | ]); 913 | }); 914 | }); 915 | 916 | //File upload is broken in Phantomjs 2.0 917 | //https://github.com/ariya/phantomjs/issues/12506 918 | it('should upload a file', function() { 919 | var horseman = new Horseman({ 920 | timeout: defaultTimeout, 921 | injectJquery: bool 922 | }); 923 | return horseman 924 | .open(serverUrl) 925 | .upload('#upload', 'test/files/testjs.js') 926 | .value('#upload') 927 | .close() 928 | .should.eventually 929 | .equal('C:\\fakepath\\testjs.js'); 930 | }); 931 | 932 | it('should download a text file', function(done) { 933 | var horseman = new Horseman({ 934 | timeout: defaultTimeout, 935 | injectJquery: bool 936 | }); 937 | return horseman 938 | .open(serverUrl) 939 | .download(serverUrl + 'test.txt') 940 | .close() 941 | .should.eventually 942 | .match(/^This is test text.\n*$/); 943 | }); 944 | 945 | it('should download a binary file', function(done) { 946 | if (phantomVersion.major < 2) { 947 | this.skip('binary .download() does not work in PhantomJS 1.0'); 948 | } 949 | var horseman = new Horseman({ 950 | timeout: defaultTimeout, 951 | injectJquery: bool 952 | }); 953 | var img = horseman 954 | .open('http://httpbin.org') 955 | .download('http://httpbin.org/image', undefined, true) 956 | .close(); 957 | var opts = {url: 'http://httpbin.org/image', encoding: null}; 958 | request(opts, function(err, res, body) { 959 | if (err) { return done(err); } 960 | img.then(function(img) { 961 | img.toString().should.equal(body.toString()); 962 | }).asCallback(done); 963 | }); 964 | }); 965 | 966 | it('should verify a file exists before upload', function() { 967 | var horseman = new Horseman({ 968 | timeout: defaultTimeout, 969 | injectJquery: bool 970 | }); 971 | return horseman 972 | .open('http://validator.w3.org/#validate_by_upload') 973 | .upload('#uploaded_file', 'nope.jpg') 974 | .close() 975 | .should 976 | .be.rejected(); 977 | }); 978 | 979 | it('should fire a keypress when typing', function() { 980 | var horseman = new Horseman({ 981 | timeout: defaultTimeout, 982 | injectJquery: bool 983 | }); 984 | return horseman 985 | .open('http://httpbin.org/forms/post') 986 | .evaluate(function() { 987 | window.keypresses = 0; 988 | var elem = document.querySelector('input[name="custname"]'); 989 | elem.onkeypress = function() { 990 | window.keypresses++; 991 | }; 992 | }) 993 | .type('input[name="custname"]', 'github') 994 | .evaluate(function() { 995 | return window.keypresses; 996 | }) 997 | .close() 998 | .should.eventually 999 | .equal(6); 1000 | }); 1001 | 1002 | it('should send mouse events', function() { 1003 | var horseman = new Horseman({ 1004 | timeout: defaultTimeout, 1005 | injectJquery: bool 1006 | }); 1007 | return horseman 1008 | .open('https://dvcs.w3.org/' + 1009 | 'hg/d4e/raw-file/tip/mouse-event-test.html') 1010 | .mouseEvent('mousedown', 200, 200) 1011 | .mouseEvent('mouseup', 200, 200) 1012 | .mouseEvent('click', 200, 200) 1013 | .mouseEvent('doubleclick', 200, 200) 1014 | .mouseEvent('mousemove', 200, 200) 1015 | .evaluate(function() { 1016 | function findByTextContent(selector, text) { 1017 | var tags = document.querySelectorAll(selector); 1018 | 1019 | var count = 0; 1020 | 1021 | for (var i = 0; i < tags.length; i++) { 1022 | if (tags[i].textContent.indexOf(text) > -1) { 1023 | count++; 1024 | } 1025 | } 1026 | return count; 1027 | } 1028 | 1029 | return { 1030 | 'mousedown': findByTextContent('tr', 'mousedown'), 1031 | 'mouseup': findByTextContent('tr', 'mouseup'), 1032 | 'click': findByTextContent('tr', 'click'), 1033 | 'doubleclick': findByTextContent('tr', 'dblclick'), 1034 | 'mousemove': findByTextContent('tr', 'mousemove'), 1035 | }; 1036 | }) 1037 | .close() 1038 | .should.eventually 1039 | .matchEach(function(eventCount) { 1040 | eventCount.should.be.greaterThan(0); 1041 | }); 1042 | }); 1043 | 1044 | it('should send keyboard events', function() { 1045 | var horseman = new Horseman({ 1046 | timeout: defaultTimeout, 1047 | injectJquery: bool 1048 | }); 1049 | return horseman 1050 | .open('http://unixpapa.com/js/testkey.html') 1051 | .keyboardEvent('keypress', 16777221) 1052 | .evaluate(function() { 1053 | return document.querySelector('textarea[name="t"]').value; 1054 | }) 1055 | .close() 1056 | .should.eventually 1057 | .match(/keyCode=13/); 1058 | }); 1059 | }); 1060 | } 1061 | 1062 | var phantomVersion; 1063 | describe('Horseman', function() { 1064 | this.timeout(20000); 1065 | 1066 | /** 1067 | * Setup an express server for testing purposes. 1068 | */ 1069 | before(function(done) { 1070 | app = express(); 1071 | var port = process.env.port || defaultPort; 1072 | app.use(express.static(path.join(__dirname, 'files'))); 1073 | server = app.listen(port, function() { 1074 | serverUrl = hostname + ':' + port + '/'; 1075 | done(); 1076 | }); 1077 | }); 1078 | 1079 | /** 1080 | * Get PhantomJS version. 1081 | * Some features require certain verrsions. 1082 | */ 1083 | before(function setupPhantom() { 1084 | var horseman = new Horseman(); 1085 | return horseman.ready 1086 | .then(function() { 1087 | return Promise.fromCallback(function(done) { 1088 | return horseman.phantom.get('version', done); 1089 | }); 1090 | }) 1091 | .then(function(version) { 1092 | phantomVersion = version; 1093 | }) 1094 | .close(); 1095 | }); 1096 | 1097 | /** 1098 | * Tear down the express server we used for testing. 1099 | */ 1100 | after(function(done) { 1101 | server.close(done); 1102 | }); 1103 | 1104 | it('should be constructable', function() { 1105 | var horseman = new Horseman(); 1106 | horseman.should.be.ok(); 1107 | return horseman.close(); 1108 | }); 1109 | 1110 | navigation(true); 1111 | 1112 | navigation(false); 1113 | 1114 | evaluation(true); 1115 | 1116 | evaluation(false); 1117 | 1118 | manipulation(true); 1119 | 1120 | manipulation(false); 1121 | 1122 | describe('Usability', function() { 1123 | it('should do a function without breaking the chain', function() { 1124 | var doComplete = false; 1125 | var horseman = new Horseman({ 1126 | timeout: defaultTimeout, 1127 | }); 1128 | return horseman 1129 | .do(function(complete) { 1130 | setTimeout(function() { 1131 | doComplete = true; 1132 | complete(); 1133 | }, 500); 1134 | }) 1135 | .close() 1136 | .then(function() { 1137 | doComplete.should.be.true(); 1138 | }); 1139 | }); 1140 | 1141 | it('should log output', function() { 1142 | var horseman = new Horseman({ 1143 | timeout: defaultTimeout, 1144 | }); 1145 | var oldLog = console.log; 1146 | var output = ''; 1147 | console.log = function(message) { 1148 | output += message; 1149 | }; 1150 | return horseman 1151 | .open(serverUrl) 1152 | .count('a') 1153 | .log() 1154 | .close() 1155 | .then(function() { 1156 | output.should.equal('1'); 1157 | }) 1158 | .finally(function() { 1159 | console.log = oldLog; 1160 | }); 1161 | }); 1162 | 1163 | it('should log falsy argument', function() { 1164 | var horseman = new Horseman({ 1165 | timeout: defaultTimeout, 1166 | }); 1167 | var oldLog = console.log; 1168 | var output = ''; 1169 | console.log = function(message) { 1170 | output += message; 1171 | }; 1172 | return horseman 1173 | .open(serverUrl) 1174 | .log(undefined) 1175 | .close() 1176 | .then(function() { 1177 | output.should.equal('undefined'); 1178 | }) 1179 | .finally(function() { 1180 | console.log = oldLog; 1181 | }); 1182 | }); 1183 | 1184 | it('should keep resolution value after log', function() { 1185 | var horseman = new Horseman({ 1186 | timeout: defaultTimeout, 1187 | }); 1188 | return horseman 1189 | .open(serverUrl) 1190 | .return(1) 1191 | .log(undefined) 1192 | .close() 1193 | .should.eventually 1194 | .equal(1); 1195 | }); 1196 | }); 1197 | 1198 | parallel('Inject jQuery', function() { 1199 | it('should inject jQuery', function() { 1200 | var horseman = new Horseman({ 1201 | timeout: defaultTimeout, 1202 | }); 1203 | return horseman 1204 | .open('http://www.google.com') 1205 | .evaluate(function() { 1206 | return typeof jQuery; 1207 | }) 1208 | .close() 1209 | .should.eventually 1210 | .equal('function'); 1211 | }); 1212 | 1213 | it('should not inject jQuery', function() { 1214 | var horseman = new Horseman({ 1215 | timeout: defaultTimeout, 1216 | injectJquery: false 1217 | }); 1218 | return horseman 1219 | .open('http://www.google.com') 1220 | .evaluate(function() { 1221 | return typeof jQuery; 1222 | }) 1223 | .close() 1224 | .should.eventually 1225 | .equal('undefined'); 1226 | }); 1227 | 1228 | it('should not stomp on existing jQuery', function() { 1229 | var horseman = new Horseman({ 1230 | timeout: defaultTimeout, 1231 | injectJquery: true 1232 | }); 1233 | return horseman 1234 | .open(serverUrl + 'jQuery.html') 1235 | .evaluate(function() { 1236 | return $.fn.jquery; 1237 | }) 1238 | .close() 1239 | .should.eventually 1240 | .equal('Not really jQuery'); 1241 | }); 1242 | }); 1243 | 1244 | parallel('Inject bluebird', function() { 1245 | it('should not inject bluebird', function() { 1246 | var horseman = new Horseman({ 1247 | timeout: defaultTimeout, 1248 | }); 1249 | return horseman 1250 | .open('http://www.google.com') 1251 | .evaluate(function() { 1252 | return typeof Promise; 1253 | }) 1254 | .close() 1255 | .should.eventually 1256 | .equal('undefined'); 1257 | }); 1258 | 1259 | it('should inject bluebird', function() { 1260 | var horseman = new Horseman({ 1261 | timeout: defaultTimeout, 1262 | injectBluebird: true 1263 | }); 1264 | return horseman 1265 | .open('http://www.google.com') 1266 | .evaluate(function() { 1267 | return typeof Promise; 1268 | }) 1269 | .close() 1270 | .should.eventually 1271 | .equal('function'); 1272 | }); 1273 | 1274 | it('should expose as Bluebird', function() { 1275 | var horseman = new Horseman({ 1276 | timeout: defaultTimeout, 1277 | injectBluebird: 'bluebird' 1278 | }); 1279 | return horseman 1280 | .open('http://www.google.com') 1281 | .evaluate(function() { 1282 | return { 1283 | Promise: typeof Promise, 1284 | Bluebird: typeof Bluebird 1285 | }; 1286 | }) 1287 | .close() 1288 | .should.eventually 1289 | .have.properties({ 1290 | Promise: 'undefined', 1291 | Bluebird: 'function' 1292 | }); 1293 | }); 1294 | 1295 | }); 1296 | 1297 | parallel('Waiting', function() { 1298 | it('should wait for the page to change', function() { 1299 | var horseman = new Horseman({ 1300 | timeout: defaultTimeout, 1301 | }); 1302 | return horseman 1303 | .open(serverUrl) 1304 | .click('a') 1305 | .waitForNextPage() 1306 | .url() 1307 | .close() 1308 | .should.eventually 1309 | .equal(serverUrl + 'next.html'); 1310 | }); 1311 | 1312 | it('should wait until a condition on the page is true', function() { 1313 | var forALink = function() { 1314 | return ($('a:contains("About")').length > 0); 1315 | }; 1316 | var horseman = new Horseman({ 1317 | timeout: defaultTimeout, 1318 | }); 1319 | return horseman 1320 | .open('http://www.google.com/') 1321 | .waitFor(forALink, true) 1322 | .evaluate(forALink) 1323 | .close() 1324 | .should.eventually 1325 | .be.true(); 1326 | }); 1327 | 1328 | it('should wait a set amount of time', function() { 1329 | var start = new Date(); 1330 | var horseman = new Horseman({ 1331 | timeout: defaultTimeout, 1332 | }); 1333 | return horseman 1334 | .open(serverUrl) 1335 | .wait(1000) 1336 | .close() 1337 | .then(function() { 1338 | var end = new Date(); 1339 | var diff = end - start; 1340 | diff.should.be.greaterThan(999); //may be a ms or so off. 1341 | }); 1342 | }); 1343 | 1344 | it('should timeout after a specific time', function() { 1345 | var start = new Date(); 1346 | var horseman = new Horseman({ 1347 | timeout: defaultTimeout, 1348 | }); 1349 | return horseman 1350 | .open(serverUrl) 1351 | .waitForSelector("#not-existing-id", { timeout : defaultTimeout/5}) 1352 | .close() 1353 | .catch(function(err){ 1354 | var end = new Date(); 1355 | var diff = end - start; 1356 | diff.should.be.below(defaultTimeout/2); //may be a ms or so off. 1357 | }); 1358 | }); 1359 | 1360 | it('should wait until a selector is seen', function() { 1361 | var horseman = new Horseman({ 1362 | timeout: defaultTimeout, 1363 | }); 1364 | return horseman 1365 | .open(serverUrl) 1366 | .waitForSelector('input') 1367 | .count('input') 1368 | .close() 1369 | .should.eventually 1370 | .be.above(0); 1371 | }); 1372 | 1373 | it('should call onTimeout if timeout in waitForNextPage', function() { 1374 | var timeoutHorseman = new Horseman({ 1375 | timeout: 10 1376 | }); 1377 | var timeoutFired = false; 1378 | return timeoutHorseman 1379 | .on('timeout', function() { 1380 | timeoutFired = true; 1381 | }) 1382 | .waitForNextPage() 1383 | .catch(Horseman.TimeoutError, function() {}) 1384 | .close() 1385 | .then(function() { 1386 | timeoutFired.should.be.true(); 1387 | }); 1388 | }); 1389 | 1390 | it('should call onTimeout if timeout in waitForNextPage with custom timeout', function() { 1391 | var start = new Date(); 1392 | var timeoutHorseman = new Horseman({ 1393 | timeout: 10 1394 | }); 1395 | var timeoutFiredTime = 0; 1396 | return timeoutHorseman 1397 | .on('timeout', function() { 1398 | var end = new Date(); 1399 | var diff = end - start; 1400 | timeoutFiredTime = diff; 1401 | }) 1402 | .waitForNextPage({timeout: 1000}) 1403 | .catch(Horseman.TimeoutError, function() {}) 1404 | .close() 1405 | .then(function() { 1406 | timeoutFiredTime.should.be.above(900); 1407 | }); 1408 | }); 1409 | 1410 | it('should reject Promise if timeout in waitForNextPage', function() { 1411 | var timeoutHorseman = new Horseman({ 1412 | timeout: 10 1413 | }); 1414 | return timeoutHorseman 1415 | .waitForNextPage() 1416 | .close() 1417 | .should 1418 | .be.rejectedWith(Horseman.TimeoutError); 1419 | }); 1420 | 1421 | it('should call onTimeout if timeout in waitForSelector', function() { 1422 | var timeoutHorseman = new Horseman({ 1423 | timeout: 10 1424 | }); 1425 | var timeoutFired = false; 1426 | return timeoutHorseman 1427 | .on('timeout', function() { 1428 | timeoutFired = true; 1429 | }) 1430 | .waitForSelector('bob') 1431 | .catch(Horseman.TimeoutError, function() {}) 1432 | .close() 1433 | .then(function() { 1434 | timeoutFired.should.be.true(); 1435 | }); 1436 | }); 1437 | 1438 | it('should reject Promise if timeout in waitForSelector', function() { 1439 | var timeoutHorseman = new Horseman({ 1440 | timeout: 10 1441 | }); 1442 | var timeoutFired = false; 1443 | return timeoutHorseman 1444 | .waitForSelector('bob') 1445 | .close() 1446 | .should 1447 | .be.rejectedWith(Horseman.TimeoutError); 1448 | }); 1449 | 1450 | it('should call onTimeout if timeout in waitFor', function() { 1451 | var timeoutHorseman = new Horseman({ 1452 | timeout: 10 1453 | }); 1454 | var timeoutFired = false; 1455 | var return5 = function() { 1456 | return 5; 1457 | }; 1458 | return timeoutHorseman 1459 | .on('timeout', function() { 1460 | timeoutFired = true; 1461 | }) 1462 | .waitFor(return5, 6) 1463 | .catch(Horseman.TimeoutError, function() {}) 1464 | .close() 1465 | .then(function() { 1466 | timeoutFired.should.be.true(); 1467 | }); 1468 | }); 1469 | 1470 | it('should reject Promise if timeout in waitFor', function() { 1471 | var timeoutHorseman = new Horseman({ 1472 | timeout: 10 1473 | }); 1474 | var timeoutFired = false; 1475 | var return5 = function() { 1476 | return 5; 1477 | }; 1478 | return timeoutHorseman 1479 | .waitFor(return5, 6) 1480 | .close() 1481 | .should 1482 | .be.rejectedWith(Horseman.TimeoutError); 1483 | }); 1484 | }); 1485 | 1486 | /** 1487 | * Iframes 1488 | */ 1489 | parallel('Frames', function() { 1490 | it('should get the frame name', function() { 1491 | var horseman = new Horseman({ 1492 | timeout: defaultTimeout, 1493 | }); 1494 | return horseman 1495 | .open(serverUrl + 'frames.html') 1496 | .switchToFrame('frame1') 1497 | .frameName() 1498 | .close() 1499 | .should.eventually 1500 | .equal('frame1'); 1501 | }); 1502 | 1503 | it('should get the frame count', function() { 1504 | var horseman = new Horseman({ 1505 | timeout: defaultTimeout, 1506 | }); 1507 | return horseman 1508 | .open(serverUrl + 'frames.html') 1509 | .frameCount() 1510 | .close() 1511 | .should.eventually 1512 | .equal(2); 1513 | }); 1514 | 1515 | it('should get frame names', function() { 1516 | var horseman = new Horseman({ 1517 | timeout: defaultTimeout, 1518 | }); 1519 | return horseman 1520 | .open(serverUrl + 'frames.html') 1521 | .frameNames() 1522 | .close() 1523 | .should.eventually 1524 | .deepEqual(['frame1', 'frame2']); 1525 | }); 1526 | 1527 | it('should let you switch to the focused frame', function() { 1528 | var horseman = new Horseman({ 1529 | timeout: defaultTimeout, 1530 | }); 1531 | return horseman 1532 | .open(serverUrl + 'frames.html') 1533 | .switchToFrame('frame1') 1534 | .switchToFocusedFrame() 1535 | .frameName() 1536 | .close() 1537 | .should.eventually 1538 | .equal(''); 1539 | }); 1540 | 1541 | it('should let you switch to a frame', function() { 1542 | var horseman = new Horseman({ 1543 | timeout: defaultTimeout, 1544 | }); 1545 | return horseman 1546 | .open(serverUrl + 'frames.html') 1547 | .switchToFrame('frame1') 1548 | .waitForSelector('h1') 1549 | .html('h1') 1550 | .close() 1551 | .should.eventually 1552 | .equal('This is frame 1.'); 1553 | }); 1554 | 1555 | it('should let you evaluate after frame switch', function() { 1556 | var horseman = new Horseman({ 1557 | timeout: defaultTimeout, 1558 | }); 1559 | return horseman 1560 | .open(serverUrl + 'frames.html') 1561 | .switchToFrame('frame1') 1562 | .waitForSelector('h1') 1563 | .evaluate(function() { 1564 | return document.querySelector('body').id; 1565 | }) 1566 | .close() 1567 | .should.eventually 1568 | .equal('f1'); 1569 | }); 1570 | 1571 | it('should let you switch to the main frame', function() { 1572 | var horseman = new Horseman({ 1573 | timeout: defaultTimeout, 1574 | }); 1575 | return horseman 1576 | .open(serverUrl + 'frames.html') 1577 | .switchToFrame('frame2') 1578 | .switchToFrame('frame31') 1579 | .switchToMainFrame() 1580 | .frameName() 1581 | .close() 1582 | .should.eventually 1583 | .equal(''); 1584 | }); 1585 | 1586 | it('should let you switch to the parent frame', function() { 1587 | var horseman = new Horseman({ 1588 | timeout: defaultTimeout, 1589 | }); 1590 | return horseman 1591 | .open(serverUrl + 'frames.html') 1592 | .switchToFrame('frame2') 1593 | .switchToFrame('frame31') 1594 | .switchToParentFrame() 1595 | .frameName() 1596 | .close() 1597 | .should.eventually 1598 | .equal('frame2'); 1599 | }); 1600 | }); 1601 | 1602 | /** 1603 | * events 1604 | */ 1605 | parallel('Events', function() { 1606 | it('should fire an event on initialized', function() { 1607 | var horseman = new Horseman({ 1608 | timeout: defaultTimeout, 1609 | }); 1610 | var fired = false; 1611 | return horseman 1612 | .on('initialized', function() { 1613 | fired = true; 1614 | }) 1615 | .open(serverUrl) 1616 | .close() 1617 | .then(function() { 1618 | fired.should.be.true(); 1619 | }); 1620 | }); 1621 | 1622 | it('should fire an event on load started', function() { 1623 | var horseman = new Horseman({ 1624 | timeout: defaultTimeout, 1625 | }); 1626 | var fired = false; 1627 | return horseman 1628 | .on('loadStarted', function() { 1629 | fired = true; 1630 | }) 1631 | .open(serverUrl) 1632 | .close() 1633 | .then(function() { 1634 | fired.should.be.true(); 1635 | }); 1636 | }); 1637 | 1638 | it('should fire an event on load finished', function() { 1639 | var horseman = new Horseman({ 1640 | timeout: defaultTimeout, 1641 | }); 1642 | var fired = false; 1643 | return horseman 1644 | .on('loadFinished', function() { 1645 | fired = true; 1646 | }) 1647 | .open(serverUrl) 1648 | .wait(50) //have to wait for the event to fire. 1649 | .close() 1650 | .then(function() { 1651 | fired.should.be.true(); 1652 | }); 1653 | }); 1654 | 1655 | it('should send status on load finished', function() { 1656 | var horseman = new Horseman({ 1657 | timeout: defaultTimeout, 1658 | }); 1659 | var stat; 1660 | return horseman 1661 | .on('loadFinished', function(status) { 1662 | stat = status; 1663 | }) 1664 | .open(serverUrl) 1665 | .wait(50) //have to wait for the event to fire. 1666 | .close() 1667 | .then(function() { 1668 | stat.should.equal('success'); 1669 | }); 1670 | }); 1671 | 1672 | it('should fire an event when a resource is requested', function() { 1673 | var horseman = new Horseman({ 1674 | timeout: defaultTimeout, 1675 | }); 1676 | var fired = false; 1677 | return horseman 1678 | .on('resourceRequested', function() { 1679 | fired = true; 1680 | }) 1681 | .open(serverUrl) 1682 | .close() 1683 | .then(function() { 1684 | fired.should.be.true(); 1685 | }); 1686 | }); 1687 | 1688 | it('should fire an event when a resource is received', function() { 1689 | var horseman = new Horseman({ 1690 | timeout: defaultTimeout, 1691 | }); 1692 | var fired = false; 1693 | return horseman 1694 | .on('resourceReceived', function() { 1695 | fired = true; 1696 | }) 1697 | .open(serverUrl) 1698 | .close() 1699 | .then(function() { 1700 | fired.should.be.true(); 1701 | }); 1702 | }); 1703 | 1704 | it('should fire an event when navigation requested', function() { 1705 | var horseman = new Horseman({ 1706 | timeout: defaultTimeout, 1707 | }); 1708 | var fired = false; 1709 | return horseman 1710 | .on('navigationRequested', 1711 | function(url, type, willNavigate, isMain) { 1712 | fired = (url === 'https://www.yahoo.com/'); 1713 | type.should.equal('Other'); 1714 | willNavigate.should.be.true(); 1715 | isMain.should.be.true(); 1716 | } 1717 | ) 1718 | .open('http://www.yahoo.com') 1719 | .close() 1720 | .then(function() { 1721 | fired.should.be.true(); 1722 | }); 1723 | }); 1724 | 1725 | it('should fire an event when the url changes', function() { 1726 | var horseman = new Horseman({ 1727 | timeout: defaultTimeout, 1728 | }); 1729 | var fired = false; 1730 | return horseman 1731 | .on('urlChanged', function() { 1732 | fired = true; 1733 | }) 1734 | .open(serverUrl) 1735 | .close() 1736 | .then(function() { 1737 | fired.should.be.true(); 1738 | }); 1739 | }); 1740 | 1741 | it('should fire an event when a console message is seen', function() { 1742 | var horseman = new Horseman({ 1743 | timeout: defaultTimeout, 1744 | }); 1745 | var fired = false; 1746 | return horseman 1747 | .on('consoleMessage', function() { 1748 | fired = true; 1749 | }) 1750 | .open(serverUrl) 1751 | .evaluate(function() { 1752 | console.log('message'); 1753 | }) 1754 | .close() 1755 | .then(function() { 1756 | fired.should.be.true(); 1757 | }); 1758 | }); 1759 | 1760 | it('should fire an event when an alert is seen', function() { 1761 | var horseman = new Horseman({ 1762 | timeout: defaultTimeout, 1763 | }); 1764 | var fired = false; 1765 | return horseman 1766 | .on('alert', function() { 1767 | fired = true; 1768 | }) 1769 | .open(serverUrl) 1770 | .evaluate(function() { 1771 | alert('ono'); 1772 | }) 1773 | .close() 1774 | .then(function() { 1775 | fired.should.be.true(); 1776 | }); 1777 | }); 1778 | 1779 | it('should fire an event when a prompt is seen', function() { 1780 | var horseman = new Horseman({ 1781 | timeout: defaultTimeout, 1782 | }); 1783 | var fired = false; 1784 | return horseman 1785 | .on('prompt', function() { 1786 | fired = true; 1787 | }) 1788 | .open(serverUrl) 1789 | .evaluate(function() { 1790 | prompt('ono'); 1791 | }) 1792 | .close() 1793 | .then(function() { 1794 | fired.should.be.true(); 1795 | }); 1796 | }); 1797 | 1798 | it('should receive return value of at callback', function() { 1799 | var horseman = new Horseman({ 1800 | timeout: defaultTimeout, 1801 | }); 1802 | return horseman 1803 | .at('prompt', function() { 1804 | return 'foo'; 1805 | }) 1806 | .open(serverUrl) 1807 | .evaluate(function() { 1808 | return prompt('ono'); 1809 | }) 1810 | .close() 1811 | .should.eventually 1812 | .equal('foo'); 1813 | }); 1814 | }); 1815 | 1816 | /** 1817 | * tabs 1818 | */ 1819 | parallel('Tabs', function() { 1820 | it('should let you open a new tab', function() { 1821 | var horseman = new Horseman({ 1822 | timeout: defaultTimeout, 1823 | }); 1824 | return horseman 1825 | .open(serverUrl) 1826 | .openTab(serverUrl + 'next.html') 1827 | .tabCount() 1828 | .close() 1829 | .should.eventually 1830 | .equal(2); 1831 | }); 1832 | 1833 | it('should fire an event when a tab is created', function() { 1834 | var horseman = new Horseman({ 1835 | timeout: defaultTimeout, 1836 | }); 1837 | var fired = false; 1838 | return horseman 1839 | .on('tabCreated', function() { 1840 | fired = true; 1841 | }) 1842 | .open(serverUrl) 1843 | .openTab(serverUrl + 'next.html') 1844 | .close() 1845 | .then(function() { 1846 | fired.should.be.true(); 1847 | }); 1848 | }); 1849 | 1850 | it('should let switch tabs', function() { 1851 | var horseman = new Horseman({ 1852 | timeout: defaultTimeout, 1853 | }); 1854 | return horseman 1855 | .open(serverUrl) 1856 | .openTab(serverUrl + 'next.html') 1857 | .switchToTab(0) 1858 | .url() 1859 | .close() 1860 | .should.eventually 1861 | .equal(serverUrl); 1862 | }); 1863 | 1864 | it('should have tabs opened by links', function() { 1865 | var horseman = new Horseman({ 1866 | timeout: defaultTimeout, 1867 | }); 1868 | return horseman 1869 | .open(serverUrl + 'opennewtab.html') 1870 | .click('a#newtab') 1871 | .waitForNextPage() 1872 | .tabCount() 1873 | .then(function(count) { 1874 | count.should.equal(2); 1875 | }) 1876 | .close(); 1877 | }); 1878 | 1879 | it('should remove closed tabs', function() { 1880 | var horseman = new Horseman({ 1881 | timeout: defaultTimeout, 1882 | }); 1883 | return horseman 1884 | .open(serverUrl + 'opennewtab.html') 1885 | .click('a#newtab') 1886 | .waitForNextPage() 1887 | .switchToTab(1) 1888 | .evaluate(function closePage() { 1889 | window.close(); 1890 | }) 1891 | .tabCount() 1892 | .close() 1893 | .should.eventually 1894 | .equal(1); 1895 | }); 1896 | 1897 | it('should close tabs', function() { 1898 | var horseman = new Horseman({ 1899 | timeout: defaultTimeout, 1900 | }); 1901 | return horseman 1902 | .open(serverUrl + 'opennewtab.html') 1903 | .click('a#newtab') 1904 | .waitForNextPage() 1905 | .switchToTab(1) 1906 | .closeTab(1) 1907 | .tabCount() 1908 | .close() 1909 | .should.eventually 1910 | .equal(1); 1911 | }); 1912 | 1913 | it('should fire an event when a tab is closed', function() { 1914 | var horseman = new Horseman({ 1915 | timeout: defaultTimeout, 1916 | }); 1917 | var fired = false; 1918 | return horseman 1919 | .on('tabClosed', function() { 1920 | fired = true; 1921 | }) 1922 | .open(serverUrl + 'opennewtab.html') 1923 | .click('a#newtab') 1924 | .waitForNextPage() 1925 | .switchToTab(1) 1926 | .closeTab(1) 1927 | .then(function() { 1928 | fired.should.be.true(); 1929 | }) 1930 | .close(); 1931 | }); 1932 | 1933 | describe('swtichToNewTab option', function() { 1934 | it('should default to not switching tab', function() { 1935 | var horseman = new Horseman({ 1936 | timeout: defaultTimeout, 1937 | }); 1938 | return horseman 1939 | .open(serverUrl + 'opennewtab.html') 1940 | .click('a#newtab') 1941 | .waitForNextPage() 1942 | .title() 1943 | .close() 1944 | .should.eventually 1945 | .equal('Horseman test link open new tab'); 1946 | }); 1947 | 1948 | it('should switch tab when true', function() { 1949 | var horseman = new Horseman({ 1950 | timeout: defaultTimeout, 1951 | switchToNewTab: true 1952 | }); 1953 | return horseman 1954 | .open(serverUrl + 'opennewtab.html') 1955 | .click('a#newtab') 1956 | .waitForNextPage() 1957 | .title() 1958 | .close() 1959 | .should.eventually 1960 | .equal('Horseman new tab'); 1961 | }); 1962 | }); 1963 | }); 1964 | 1965 | describe('Chaining', function() { 1966 | var horseman; 1967 | beforeEach(function() { 1968 | horseman = new Horseman({ 1969 | timeout: defaultTimeout, 1970 | }); 1971 | }); 1972 | 1973 | afterEach(function() { 1974 | return horseman.close(); 1975 | }); 1976 | 1977 | it('should be available when calling actions on horseman', function() { 1978 | var p = horseman.open('about:blank'); 1979 | return p.finally(function() { 1980 | p.should.have.properties(Object.keys(actions)); 1981 | }); 1982 | }); 1983 | 1984 | it('should be available when calling actions on Promises', function() { 1985 | var p = horseman.open('about:blank').url(); 1986 | return p.finally(function() { 1987 | p.should.have.properties(Object.keys(actions)); 1988 | }); 1989 | }); 1990 | 1991 | it('should be available when calling Promise methods', function() { 1992 | var p = horseman.open('about:blank').then(function() {}); 1993 | return p.finally(function() { 1994 | p.should.have.properties(Object.keys(actions)); 1995 | }); 1996 | }); 1997 | 1998 | it('should call close after rejection', function() { 1999 | // Record if close gets called 2000 | var close = horseman.close; 2001 | var called = false; 2002 | horseman.close = function() { 2003 | called = true; 2004 | return close.apply(this, arguments); 2005 | }; 2006 | return horseman 2007 | .open('about:blank') 2008 | .then(function() { 2009 | throw new Error('Intentional Rejection'); 2010 | }) 2011 | .close() 2012 | .catch(function() {}) 2013 | .finally(function() { 2014 | called.should.be.true(); 2015 | }) 2016 | .then(function() { 2017 | // Don't call close twice 2018 | horseman.close = function() {}; 2019 | }); 2020 | }); 2021 | 2022 | it('should not call exit in close after failed init', function() { 2023 | var BAD_PATH = 'notphantom'; 2024 | var horseman = new Horseman({phantomPath: BAD_PATH}); 2025 | return horseman 2026 | .open('about:blank') 2027 | .close() 2028 | .should 2029 | .be.rejectedWith({code: 'ENOENT'}); 2030 | }); 2031 | }); 2032 | 2033 | describe('Proxy', function() { 2034 | var proxy; 2035 | var PROXY_HEADER = 'x-horseman-proxied'; 2036 | var PROXY_PORT = process.env['proxy_port'] || 2037 | (process.env.port || defaultPort) + 1; 2038 | 2039 | // Set up proxy server for phantom to connect to 2040 | before(function setupProxy(done) { 2041 | if (!semver.satisfies(process.version, '>= 0.12')) { 2042 | this.skip('text proxy server require Node 0.12 or above'); 2043 | } 2044 | proxy = hoxy.createServer(); 2045 | proxy.intercept('response', function(req, resp) { 2046 | resp.headers[PROXY_HEADER] = 'test'; 2047 | }); 2048 | proxy.listen(PROXY_PORT, done); 2049 | }); 2050 | 2051 | after(function closeProxy(done) { 2052 | if (proxy) { 2053 | proxy.close(done); 2054 | } else { 2055 | done(); 2056 | } 2057 | }); 2058 | 2059 | it('should use arguments', function() { 2060 | var horseman = new Horseman({ 2061 | timeout: defaultTimeout, 2062 | proxy: 'localhost:' + PROXY_PORT, 2063 | proxyType: 'http', 2064 | }); 2065 | var hadHeader = false; 2066 | return horseman 2067 | .on('resourceReceived', function(resp) { 2068 | var hasHeader = resp.headers.some(function(header) { 2069 | return header.name === PROXY_HEADER; 2070 | }); 2071 | hadHeader = hadHeader || hasHeader; 2072 | }) 2073 | .open('http://www.google.com') 2074 | .close() 2075 | .then(function() { 2076 | hadHeader.should.equal(true); 2077 | }); 2078 | }); 2079 | 2080 | it('should use setProxy', function() { 2081 | var horseman = new Horseman({ 2082 | timeout: defaultTimeout, 2083 | }); 2084 | if (phantomVersion.major < 2) { 2085 | this.skip('setProxy requires PhantomJS 2.0 or greater'); 2086 | } 2087 | var hadHeader = false; 2088 | return horseman 2089 | .setProxy('localhost', PROXY_PORT) 2090 | .on('resourceReceived', function(resp) { 2091 | var hasHeader = resp.headers.some(function(header) { 2092 | return header.name === PROXY_HEADER; 2093 | }); 2094 | hadHeader = hadHeader || hasHeader; 2095 | }) 2096 | .open('http://www.google.com') 2097 | .then(function() { 2098 | hadHeader.should.equal(true); 2099 | }) 2100 | .close(); 2101 | }); 2102 | }); 2103 | 2104 | describe('Cache', function() { 2105 | 2106 | describe('diskCache and diskCachePath options', function() { 2107 | var CACHE_PATH = path.join(os.tmpdir(), 'test_horseman_cache'); 2108 | var rmCachePath = function(done) { 2109 | if (fs.existsSync(CACHE_PATH)) { 2110 | rmdir(CACHE_PATH, done); 2111 | } else { 2112 | done(); 2113 | } 2114 | }; 2115 | 2116 | before(rmCachePath); 2117 | 2118 | after(rmCachePath); 2119 | 2120 | it('should cache files on disk', function() { 2121 | if (phantomVersion.major < 2) { 2122 | this.skip('diskCachePath requires PhantomJS 2.0 or greater'); 2123 | } 2124 | 2125 | var horseman = new Horseman({ 2126 | diskCache: true, 2127 | diskCachePath: CACHE_PATH 2128 | }); 2129 | 2130 | return horseman 2131 | .open(serverUrl) 2132 | .then(function () { 2133 | fs.existsSync(CACHE_PATH).should.be.true(); 2134 | fs.readdirSync(CACHE_PATH).some(function (dirName) { 2135 | return dirName.match(/data\d+/); 2136 | }).should.be.true(); 2137 | }) 2138 | .close(); 2139 | }) 2140 | }) 2141 | }); 2142 | }); 2143 | --------------------------------------------------------------------------------