├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bridge.js ├── headless_error.js ├── node-phantom-simple.js ├── package.json └── test ├── .eslintrc ├── fixtures ├── injecttest.js ├── modifytest.js ├── uploadtest.txt └── verifyrender.png ├── helpers.js ├── mocha.opts ├── test_engine_bad_path.js ├── test_engine_command_line_options.js ├── test_engine_create.js ├── test_engine_get_hierarchical.js ├── test_engine_injectjs.js ├── test_engine_unexpected_exit.js ├── test_page_create.js ├── test_page_evaluate.js ├── test_page_include_js.js ├── test_page_open.js ├── test_page_push_notifications.js ├── test_page_release.js ├── test_page_render.js ├── test_page_send_event.js ├── test_page_set_fn.js ├── test_page_set_get.js ├── test_page_set_get_hierarchical.js ├── test_page_upload_file.js └── test_page_wait_for_selector.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | browser: true 4 | es6: false 5 | 6 | rules: 7 | accessor-pairs: 2 8 | array-bracket-spacing: [ 2, "always", { "singleValue": true, "objectsInArrays": true, "arraysInArrays": true } ] 9 | block-scoped-var: 2 10 | block-spacing: 2 11 | brace-style: [ 2, '1tbs', { allowSingleLine: true } ] 12 | # Postponed 13 | #callback-return: 2 14 | comma-dangle: 2 15 | comma-spacing: 2 16 | comma-style: 2 17 | computed-property-spacing: [ 2, never ] 18 | consistent-this: [ 2, self ] 19 | consistent-return: 2 20 | # ? change to multi 21 | curly: [ 2, 'multi-line' ] 22 | dot-notation: 2 23 | eol-last: 2 24 | eqeqeq: 2 25 | #func-style: [ 2, declaration ] 26 | # Postponed 27 | #global-require: 2 28 | guard-for-in: 2 29 | handle-callback-err: 2 30 | 31 | indent: [ 2, 2, { VariableDeclarator: { var: 2, let: 2, const: 3 }, SwitchCase: 1 } ] 32 | 33 | # key-spacing: [ 2, { "align": "value" } ] 34 | keyword-spacing: 2 35 | linebreak-style: 2 36 | max-depth: [ 1, 6 ] 37 | #max-nested-callbacks: [ 1, 4 ] 38 | # string can exceed 80 chars, but should not overflow github website :) 39 | #max-len: [ 2, 120, 1000 ] 40 | new-cap: 2 41 | new-parens: 2 42 | # Postponed 43 | #newline-after-var: 2 44 | no-alert: 2 45 | no-array-constructor: 2 46 | no-bitwise: 2 47 | no-caller: 2 48 | #no-case-declarations: 2 49 | no-catch-shadow: 2 50 | no-cond-assign: 2 51 | no-console: 1 52 | no-constant-condition: 2 53 | no-control-regex: 2 54 | no-debugger: 2 55 | no-delete-var: 2 56 | no-div-regex: 2 57 | no-dupe-args: 2 58 | no-dupe-keys: 2 59 | no-duplicate-case: 2 60 | no-else-return: 2 61 | # Tend to drop 62 | # no-empty: 1 63 | no-empty-character-class: 2 64 | no-empty-pattern: 2 65 | no-eq-null: 2 66 | #no-eval: 2 67 | no-ex-assign: 2 68 | no-extend-native: 2 69 | no-extra-bind: 2 70 | no-extra-boolean-cast: 2 71 | no-extra-semi: 2 72 | no-fallthrough: 2 73 | no-floating-decimal: 2 74 | no-func-assign: 2 75 | # Postponed 76 | #no-implicit-coercion: [2, { "boolean": true, "number": true, "string": true } ] 77 | no-implied-eval: 2 78 | no-inner-declarations: 2 79 | no-invalid-regexp: 2 80 | no-irregular-whitespace: 2 81 | no-iterator: 2 82 | no-label-var: 2 83 | no-labels: 2 84 | no-lone-blocks: 2 85 | no-lonely-if: 2 86 | no-loop-func: 2 87 | no-mixed-requires: 2 88 | no-mixed-spaces-and-tabs: 2 89 | # Postponed 90 | #no-native-reassign: 2 91 | no-negated-in-lhs: 2 92 | # Postponed 93 | #no-nested-ternary: 2 94 | no-new: 2 95 | no-new-func: 2 96 | no-new-object: 2 97 | no-new-require: 2 98 | no-new-wrappers: 2 99 | no-obj-calls: 2 100 | no-octal: 2 101 | no-octal-escape: 2 102 | no-path-concat: 2 103 | no-proto: 2 104 | no-redeclare: 2 105 | # Postponed 106 | #no-regex-spaces: 2 107 | no-return-assign: 2 108 | no-self-compare: 2 109 | no-sequences: 2 110 | # no-shadow: 2 111 | no-shadow-restricted-names: 2 112 | no-sparse-arrays: 2 113 | no-trailing-spaces: 2 114 | no-undef: 2 115 | no-undef-init: 2 116 | no-undefined: 2 117 | no-unexpected-multiline: 2 118 | no-unreachable: 2 119 | no-unused-expressions: 2 120 | no-unused-vars: 2 121 | #no-use-before-define: 2 122 | no-void: 2 123 | no-with: 2 124 | object-curly-spacing: [ 2, always, { "objectsInObjects": true, "arraysInObjects": true } ] 125 | operator-assignment: 1 126 | # Postponed 127 | #operator-linebreak: [ 2, after ] 128 | semi: 2 129 | semi-spacing: 2 130 | space-before-function-paren: [ 2, { "anonymous": "always", "named": "never" } ] 131 | space-in-parens: [ 2, never ] 132 | space-infix-ops: 2 133 | space-unary-ops: 2 134 | # Postponed 135 | #spaced-comment: [ 1, always, { exceptions: [ '/', '=' ] } ] 136 | strict: [ 2, global ] 137 | quotes: [ 2, single, avoid-escape ] 138 | quote-props: [ 1, 'as-needed', { "keywords": true } ] 139 | radix: 2 140 | use-isnan: 2 141 | valid-typeof: 2 142 | yoda: [ 2, never, { "exceptRange": true } ] 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | node_modules 14 | 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - "sh -e /etc/init.d/xvfb start" 8 | script: npm run test-phantom 9 | sudo: false 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.2.4 / 2016-01-26 2 | ------------------ 3 | 4 | - Fix: parse 127.0.0.0/8 properly, #115, thanks to @janl. 5 | 6 | 7 | 2.2.3 / 2016-01-17 8 | ------------------ 9 | 10 | - Allow engine options to be an array to pass in verbatim keys and values. 11 | 12 | 13 | 2.2.2 / 2015-12-30 14 | ------------------ 15 | 16 | - One more improvement of phantom exit check, #102, thanks to @siboulet. 17 | 18 | 19 | 2.2.1 / 2015-12-09 20 | ------------------ 21 | 22 | - Improved phantom die detection (connection loss), #100, thanks to @joscha. 23 | 24 | 25 | 2.2.0 / 2015-11-28 26 | ------------------ 27 | 28 | - Don't write messages to console - use `debug` instead, #96. 29 | 30 | 31 | 2.1.1 / 2015-11-07 32 | ------------------ 33 | 34 | - Improved port detection support for DO droplets, #76, thanks to @svvac. 35 | 36 | 37 | 2.1.0 / 2015-10-28 38 | ------------------ 39 | 40 | - Replaced SIGINT/SIGTERM handlers with watchdog, #81. 41 | - Improved nested page props support, #90. 42 | - Added support for `page.header.contents` & `page.footer.contents`, #78. 43 | 44 | 45 | 2.0.6 / 2015-10-09 46 | ------------------ 47 | 48 | - Added `.setProxy()` support. 49 | 50 | 51 | 2.0.5 / 2015-09-17 52 | ------------------ 53 | 54 | - Removed uncaught exception handler and forced exit (that should be in parent 55 | application) 56 | - Added process events proxy to avoid memleak warnings on parallel browser 57 | instances run. 58 | - Added `onConfirm` details to known issues. 59 | 60 | 61 | 2.0.4 / 2015-08-22 62 | ------------------ 63 | 64 | - Fixed poll loop termination after browser die (missed in 2.0.3). 65 | 66 | 67 | 2.0.3 / 2015-08-17 68 | ------------------ 69 | 70 | - Fixed poll loop termination after `.close()`. 71 | 72 | 73 | 2.0.2 / 2015-08-02 74 | ------------------ 75 | 76 | - Added `clearMemoryCache()` engine method support. 77 | - Coding style update. 78 | - Added linter to tests. 79 | 80 | 81 | 2.0.1 / 2015-07-12 82 | ------------------ 83 | 84 | - Improved iproute2 support (different output format in Ubuntu 15.04+). 85 | 86 | 87 | 2.0.0 / 2015-07-09 88 | ------------------ 89 | 90 | - Added SlimerJS support. 91 | - Moved callbacks to last position in all functions. 92 | - old style calls still work but show deprecation message. 93 | - Renamed `options.phantomPath` -> `options.path` 94 | - old style options still work but show deprecation message. 95 | - Added FreeBSD support. 96 | - Improved Linux support - try iproute2 before net-tools. 97 | - Added dot notation support for nested properties in `.set` / `.get`. 98 | - Defined missed `onResourceTimeout` handler. 99 | - Defined `onAuthPrompt` handler, specific to SlimerJS. 100 | - Fixed Win32 support. 101 | - Fixed Yosemite support with multiname localhost aliases. 102 | - Return proper errors when PhantomJS / SlimerJS process dies. 103 | - Fixed `waitForSelector` callback result. 104 | - Fixed possible result corruption in `evaluate`. 105 | - Rewritten tests & automated testing. 106 | 107 | 108 | 1.2.0 / 2014-03-19 109 | ------------------ 110 | 111 | - Tests rewrite & code cleanup. 112 | 113 | 114 | 1.1.1 / 2014-03-12 115 | ------------------ 116 | 117 | - Fix possible ECONNRESET after exit. 118 | 119 | 120 | 1.1.0 / 2014-02-10 121 | ------------------ 122 | 123 | - Fix and work-around broken includeJs function. 124 | 125 | 126 | Previous versions 127 | ----------------- 128 | 129 | ... 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 Matt Sergeant 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-phantom-simple 2 | =================== 3 | 4 | [![Build Status](https://img.shields.io/travis/baudehlo/node-phantom-simple/master.svg?style=flat)](https://travis-ci.org/baudehlo/node-phantom-simple) 5 | [![NPM version](https://img.shields.io/npm/v/node-phantom-simple.svg?style=flat)](https://www.npmjs.org/package/node-phantom-simple) 6 | 7 | > A bridge between [PhantomJS](http://phantomjs.org/) / [SlimerJS](https://slimerjs.org/) 8 | and [Node.js](http://nodejs.org/). 9 | 10 | This module is API-compatible with 11 | [node-phantom](https://www.npmjs.com/package/node-phantom) but doesn't rely on 12 | `WebSockets` / `socket.io`. In essence the communication between Node and 13 | Phantom / Slimer has been simplified significantly. It has the following advantages 14 | over `node-phantom`: 15 | 16 | - Fewer dependencies/layers. 17 | - Doesn't use the unreliable and huge socket.io. 18 | - Works under [`cluster`](http://nodejs.org/api/cluster.html) (node-phantom 19 | does not, due to [how it works](https://nodejs.org/api/cluster.html#cluster_how_it_works)) 20 | `server.listen(0)` works in cluster. 21 | - Supports SlimerJS. 22 | 23 | 24 | Migrating 1.x -> 2.x 25 | -------------------- 26 | 27 | Your software should work without changes, but can show deprecation warning 28 | about outdated signatures. You need to update: 29 | 30 | - `options.phantomPath` -> `options.path` 31 | - in `.create()` `.evaluate()` & `.waitForSelector()` -> move `callback` to last 32 | position of arguments list. 33 | 34 | That's all! 35 | 36 | 37 | Installing 38 | ---------- 39 | 40 | ```bash 41 | npm install node-phantom-simple 42 | 43 | # Also need phantomjs OR slimerjs: 44 | 45 | npm install phantomjs 46 | # OR 47 | npm install slimerjs 48 | ``` 49 | 50 | __Note__. SlimerJS is not headless and requires a windowing environment. 51 | Under Linux/FreeBSD/OSX [xvfb can be used to run headlessly.](https://docs.slimerjs.org/current/installation.html#having-a-headless-slimerjs). For example, if you wish 52 | to run SlimerJS on Travis-CI, add those lines to your `.travis.yml` config: 53 | 54 | ```yaml 55 | before_script: 56 | - export DISPLAY=:99.0 57 | - "sh -e /etc/init.d/xvfb start" 58 | ``` 59 | 60 | 61 | Development 62 | ----------- 63 | 64 | You should manualy install `slimerjs` to run `npm test`: 65 | 66 | ```bash 67 | npm install slimerjs 68 | ``` 69 | 70 | It's excluded from devDeps, because slimerjs binary download is banned on 71 | Tvavice-CI network by authors. 72 | 73 | 74 | Usage 75 | ----- 76 | 77 | You can use it exactly like node-phantom, and the entire API of PhantomJS 78 | should work, with the exception that every method call takes a callback (always 79 | as the last parameter), instead of returning values. 80 | 81 | For example, this is an adaptation of a 82 | [web scraping example](http://net.tutsplus.com/tutorials/javascript-ajax/web-scraping-with-node-js/): 83 | 84 | ```js 85 | var driver = require('node-phantom-simple'); 86 | 87 | driver.create({ path: require('phantomjs').path }, function (err, browser) { 88 | return browser.createPage(function (err, page) { 89 | return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function (err,status) { 90 | console.log("opened site? ", status); 91 | page.includeJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function (err) { 92 | // jQuery Loaded. 93 | // Wait for a bit for AJAX content to load on the page. Here, we are waiting 5 seconds. 94 | setTimeout(function () { 95 | return page.evaluate(function () { 96 | //Get what you want from the page using jQuery. A good way is to populate an object with all the jQuery commands that you need and then return the object. 97 | var h2Arr = [], 98 | pArr = []; 99 | 100 | $('h2').each(function () { h2Arr.push($(this).html()); }); 101 | $('p').each(function () { pArr.push($(this).html()); }); 102 | 103 | return { 104 | h2: h2Arr, 105 | p: pArr 106 | }; 107 | }, function (err,result) { 108 | console.log(result); 109 | browser.exit(); 110 | }); 111 | }, 5000); 112 | }); 113 | }); 114 | }); 115 | }); 116 | ``` 117 | 118 | ### .create(options, callback) 119 | 120 | __options__ (not mandatory): 121 | 122 | - __path__ (String) - path to phantomjs/slimerjs, if not set - will search in $PATH 123 | - __parameters__ (Array) - CLI params for executed engine, [ { nave: value } ]. 124 | You can also pass in an array to use verbatim names and values. 125 | - __ignoreErrorPattern__ (RegExp) - a regular expression that can be used to 126 | silence spurious warnings in console, generated by Qt and PhantomJS. 127 | On Mavericks, you can use `/CoreText/` to suppress some common annoying 128 | font-related warnings. 129 | 130 | 131 | For example 132 | 133 | ```js 134 | driver.create({ parameters: { 'ignore-ssl-errors': 'yes' } }, callback) 135 | driver.create({ parameters: ['-jsconsole', '-P', 'myVal']} }, callback) 136 | ``` 137 | 138 | will start phantom as: 139 | 140 | ```bash 141 | phantomjs --ignore-ssl-errors=yes 142 | ``` 143 | 144 | You can rely on globally installed engines, but we recommend to pass path explicit: 145 | 146 | ```js 147 | driver.create({ path: require('phantomjs').path }, callback) 148 | // or for slimer 149 | driver.create({ path: require('slimerjs').path }, callback) 150 | ``` 151 | 152 | You can also have a look at [the test directory](tests/) to see some examples 153 | of using the API, however the de-facto reference is the 154 | [PhantomJS documentation](https://github.com/ariya/phantomjs/wiki/API-Reference). 155 | Just mentally substitute all return values for callbacks. 156 | 157 | 158 | WebPage Callbacks 159 | ----------------- 160 | 161 | All of the `WebPage` callbacks have been implemented including `onCallback`, 162 | and are set the same way as with the core phantomjs library: 163 | 164 | ```js 165 | page.onResourceReceived = function(response) { 166 | console.log('Response (#' + response.id + ', stage "' + response.stage + '"): ' + JSON.stringify(response)); 167 | }; 168 | ``` 169 | 170 | This includes the `onPageCreated` callback which receives a new `page` object. 171 | 172 | 173 | Properties 174 | ---------- 175 | 176 | Properties on the [WebPage](https://github.com/ariya/phantomjs/wiki/API-Reference-WebPage) 177 | and [Phantom](https://github.com/ariya/phantomjs/wiki/API-Reference-phantom) 178 | objects are accessed via the `get()`/`set()` method calls: 179 | 180 | ```js 181 | page.get('content', function (err, html) { 182 | console.log("Page HTML is: " + html); 183 | }); 184 | 185 | page.set('zoomfactor', 0.25, function () { 186 | page.render('capture.png'); 187 | }); 188 | 189 | // You can get/set nested values easy! 190 | page.set('settings.userAgent', 'PhAnToSlImEr', callback); 191 | ``` 192 | 193 | 194 | Known issues 195 | ------------ 196 | 197 | Engines are buggy. Here are some cases you should know. 198 | 199 | - `.evaluate` can return corrupted result: 200 | - SlimerJS: undefined -> null. 201 | - PhantomJS: 202 | - undefined -> null 203 | - null -> '' (empty string) 204 | - [ 1, undefined, 2 ] -> null 205 | - `page.onConfirm()` handler can not return value due async driver nature. 206 | Use `.setFn()` instead: `page.setFn('onConfirm', function () { return true; })`. 207 | 208 | License 209 | ------- 210 | 211 | [MIT](https://github.com/baudehlo/node-phantom-simple/blob/master/LICENSE) 212 | 213 | 214 | Other 215 | ----- 216 | 217 | Made by Matt Sergeant for Hubdoc Inc. 218 | -------------------------------------------------------------------------------- /bridge.js: -------------------------------------------------------------------------------- 1 | /*global phantom*/ 2 | /*eslint-disable strict*/ 3 | var webpage = require('webpage'); 4 | var webserver = require('webserver').create(); 5 | var system = require('system'); 6 | 7 | var pages = {}; 8 | var page_id = 1; 9 | 10 | var callback_stack = []; 11 | 12 | // Max interval without requests from master process 13 | var WATCHDOG_TIMEOUT = 30000; 14 | 15 | phantom.onError = function (msg, trace) { 16 | var msgStack = [ 'PHANTOM ERROR: ' + msg ]; 17 | 18 | if (trace && trace.length) { 19 | msgStack.push('TRACE:'); 20 | trace.forEach(function (t) { 21 | msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : '')); 22 | }); 23 | } 24 | 25 | system.stderr.writeLine(msgStack.join('\n')); 26 | phantom.exit(1); 27 | }; 28 | 29 | var watchdog_timer_id = null; 30 | 31 | // Kill phantom if parent disconnected 32 | function watchdog_clear() { 33 | clearTimeout(watchdog_timer_id); 34 | 35 | watchdog_timer_id = setTimeout(function () { 36 | phantom.exit(0); 37 | }, WATCHDOG_TIMEOUT); 38 | } 39 | 40 | function lookup(obj, key, value) { 41 | // key can be either string or an array of strings 42 | if (!(typeof obj === 'object')) return null; 43 | 44 | if (typeof key === 'string') key = key.split('.'); 45 | 46 | if (!Array.isArray(key)) return null; 47 | 48 | if (arguments.length > 2) { 49 | if (key.length === 1) obj[key[0]] = value; 50 | else obj[key[0]] = lookup(typeof obj[key[0]] === 'object' ? obj[key[0]] : {}, key.slice(1), value); 51 | 52 | return obj; 53 | } 54 | 55 | if (key.length === 1) return obj[key[0]]; 56 | 57 | return lookup(obj[key[0]], key.slice(1)); 58 | } 59 | 60 | function page_open(res, page, args) { 61 | page.open.apply(page, args.concat(function (success) { 62 | res.statusCode = 200; 63 | res.setHeader('Content-Type', 'application/json'); 64 | res.write(JSON.stringify({ data: success })); 65 | res.close(); 66 | })); 67 | } 68 | 69 | function include_js(res, page, args) { 70 | res.statusCode = 200; 71 | res.setHeader('Content-Type', 'application/json'); 72 | res.write(JSON.stringify({ data: 'success' })); 73 | 74 | page.includeJs.apply(page, args.concat(function () { 75 | try { 76 | res.write(''); 77 | res.close(); 78 | } catch (e) { 79 | if (!/cannot call function of deleted QObject/.test(e)) { // Ignore this error 80 | page.onError(e); 81 | } 82 | } 83 | })); 84 | } 85 | 86 | webserver.listen('127.0.0.1:0', function (req, res) { 87 | // Update watchdog timer on every request 88 | watchdog_clear(); 89 | 90 | if (req.method === 'GET') { 91 | res.statusCode = 200; 92 | res.setHeader('Content-Type', 'application/json'); 93 | res.write(JSON.stringify({ data: callback_stack })); 94 | callback_stack = []; 95 | res.close(); 96 | } else if (req.method === 'POST') { 97 | var request, error, output; 98 | 99 | try { 100 | request = JSON.parse(req.post); 101 | } catch (err) { 102 | error = err; 103 | } 104 | 105 | if (!error) { 106 | if (request.page) { 107 | if (request.method === 'open') { // special case this as it's the only one with a callback 108 | page_open(res, pages[request.page], request.args); 109 | return; 110 | } else if (request.method === 'includeJs') { 111 | include_js(res, pages[request.page], request.args); 112 | return; 113 | } 114 | try { 115 | output = pages[request.page][request.method].apply(pages[request.page], request.args); 116 | } catch (err) { 117 | error = err; 118 | } 119 | } else { 120 | try { 121 | output = global_methods[request.method].apply(global_methods, request.args); 122 | } catch (err) { 123 | error = err; 124 | } 125 | } 126 | } 127 | 128 | res.setHeader('Content-Type', 'application/json'); 129 | if (error) { 130 | res.statusCode = 500; 131 | res.write(JSON.stringify(error)); 132 | } else { 133 | res.statusCode = 200; 134 | res.write(JSON.stringify({ data: output })); 135 | } 136 | res.close(); 137 | } else { 138 | throw 'Unknown request type!'; 139 | } 140 | }); 141 | 142 | var callbacks = [ 143 | 'onAlert', 'onCallback', 'onClosing', 'onConfirm', 'onConsoleMessage', 'onError', 'onFilePicker', 144 | 'onInitialized', 'onLoadFinished', 'onLoadStarted', 'onNavigationRequested', 145 | 'onPrompt', 'onResourceRequested', 'onResourceReceived', 'onResourceTimeout', 'onResourceError', 'onUrlChanged', 146 | // SlimerJS only 147 | 'onAuthPrompt' 148 | ]; 149 | 150 | function setup_callbacks(id, page) { 151 | callbacks.forEach(function (cb) { 152 | page[cb] = function (parm) { 153 | var args = Array.prototype.slice.call(arguments); 154 | 155 | if ((cb === 'onResourceRequested') && (parm.url.indexOf('data:image') === 0)) { 156 | return; 157 | } 158 | 159 | if (cb === 'onClosing') { args = []; } 160 | callback_stack.push({ page_id: id, callback: cb, args: args }); 161 | }; 162 | }); 163 | // Special case this 164 | page.onPageCreated = function (page) { 165 | var new_id = setup_page(page); 166 | callback_stack.push({ page_id: id, callback: 'onPageCreated', args: [ new_id ] }); 167 | }; 168 | } 169 | 170 | function setup_page(page) { 171 | var id = page_id++; 172 | page.getProperty = function (prop) { 173 | return lookup(page, prop); 174 | }; 175 | page.setProperty = function (prop, val) { 176 | // Special case for `paperSize.header.contents` property. 177 | if (prop === 'paperSize.header.contents' && val) { 178 | val = phantom.callback(eval('(' + val + ')')); 179 | } else if (prop === 'paperSize.header' && val.contents) { 180 | val.contents = phantom.callback(eval('(' + val.contents + ')')); 181 | } else if (prop === 'paperSize' && val.header && val.header.contents) { 182 | val.header.contents = phantom.callback(eval('(' + val.header.contents + ')')); 183 | } 184 | 185 | // Special case for `paperSize.footer.contents` property. 186 | if (prop === 'paperSize.footer.contents' && val) { 187 | val = phantom.callback(eval('(' + val + ')')); 188 | } else if (prop === 'paperSize.footer' && val.contents) { 189 | val.contents = phantom.callback(eval('(' + val.contents + ')')); 190 | } else if (prop === 'paperSize' && val.footer && val.footer.contents) { 191 | val.footer.contents = phantom.callback(eval('(' + val.footer.contents + ')')); 192 | } 193 | 194 | lookup(page, prop, val); 195 | return true; 196 | }; 197 | page.setFunction = function (name, fn) { 198 | page[name] = eval('(' + fn + ')'); 199 | return true; 200 | }; 201 | pages[id] = page; 202 | setup_callbacks(id, page); 203 | return id; 204 | } 205 | 206 | var global_methods = { 207 | setProxy: function (ip, port, proxyType, user, password) { 208 | return phantom.setProxy(ip, port, proxyType, user, password); 209 | }, 210 | createPage: function () { 211 | var page = webpage.create(); 212 | var id = setup_page(page); 213 | return { page_id: id }; 214 | }, 215 | 216 | injectJs: function (filename) { 217 | return phantom.injectJs(filename); 218 | }, 219 | 220 | exit: function (code) { 221 | return phantom.exit(code); 222 | }, 223 | 224 | addCookie: function (cookie) { 225 | return phantom.addCookie(cookie); 226 | }, 227 | 228 | clearCookies: function () { 229 | return phantom.clearCookies(); 230 | }, 231 | 232 | deleteCookie: function (name) { 233 | return phantom.deleteCookie(name); 234 | }, 235 | 236 | getProperty: function (prop) { 237 | return lookup(phantom, prop); 238 | }, 239 | 240 | setProperty: function (prop, value) { 241 | lookup(phantom, prop, value); 242 | return true; 243 | } 244 | }; 245 | 246 | // Start watchdog timer 247 | watchdog_clear(); 248 | 249 | /*eslint-disable no-console*/ 250 | console.log('Ready [' + system.pid + '] [' + webserver.port + ']'); 251 | -------------------------------------------------------------------------------- /headless_error.js: -------------------------------------------------------------------------------- 1 | // Error class 2 | // 3 | // Based on: 4 | // http://stackoverflow.com/questions/8458984/how-do-i-get-a-correct-backtrace-for-a-custom-error-class-in-nodejs 5 | // 6 | 'use strict'; 7 | 8 | 9 | var inherits = require('util').inherits; 10 | 11 | 12 | function HeadlessError(message) { 13 | // Super constructor 14 | Error.call(this); 15 | 16 | // Super helper method to include stack trace in error object 17 | Error.captureStackTrace(this, this.constructor); 18 | 19 | // Set our function’s name as error name 20 | this.name = this.constructor.name; 21 | 22 | // Set the error message 23 | this.message = message; 24 | } 25 | 26 | 27 | // Inherit from Error 28 | inherits(HeadlessError, Error); 29 | 30 | 31 | module.exports = HeadlessError; 32 | -------------------------------------------------------------------------------- /node-phantom-simple.js: -------------------------------------------------------------------------------- 1 | /*global document*/ 2 | 3 | 'use strict'; 4 | 5 | 6 | var HeadlessError = require('./headless_error'); 7 | var http = require('http'); 8 | var spawn = require('child_process').spawn; 9 | var exec = require('child_process').exec; 10 | var util = require('util'); 11 | var path = require('path'); 12 | var debug = require('debug'); 13 | 14 | var POLL_INTERVAL = process.env.POLL_INTERVAL || 500; 15 | 16 | var logger = { 17 | debug: debug('node-phantom-simple:debug'), 18 | warn: debug('node-phantom-simple:warn'), 19 | error: debug('node-phantom-simple:error') 20 | }; 21 | 22 | var queue = function (worker) { 23 | var _q = []; 24 | var running = false; 25 | var q = { 26 | push: function (obj) { 27 | _q.push(obj); 28 | q.process(); 29 | }, 30 | process: function () { 31 | if (running || _q.length === 0) { return; } 32 | running = true; 33 | var cb = function () { 34 | running = false; 35 | q.process(); 36 | }; 37 | var task = _q.shift(); 38 | worker(task, cb); 39 | } 40 | }; 41 | 42 | return q; 43 | }; 44 | 45 | function callbackOrDummy(callback, poll_func) { 46 | if (!callback) { return function () {}; } 47 | 48 | if (poll_func) { 49 | return function () { 50 | var args = Array.prototype.slice.call(arguments); 51 | 52 | poll_func(function (err) { 53 | if (err) { 54 | // We could send back the original arguments, 55 | // but I'm assuming that this error is better. 56 | callback(err); 57 | return; 58 | } 59 | 60 | callback.apply(null, args); 61 | }); 62 | }; 63 | } 64 | 65 | return callback; 66 | } 67 | 68 | function unwrapArray(arr) { 69 | return arr && arr.length === 1 ? arr[0] : arr; 70 | } 71 | 72 | function wrapArray(arr) { 73 | // Ensure that arr is an Array 74 | return (arr instanceof Array) ? arr : [ arr ]; 75 | } 76 | 77 | function clone(obj) { 78 | if (obj === null || typeof obj !== 'object') { 79 | return obj; 80 | } 81 | 82 | var copy = {}; 83 | 84 | for (var attr in obj) { 85 | if (obj.hasOwnProperty(attr)) { 86 | copy[attr] = clone(obj[attr]); 87 | } 88 | } 89 | 90 | return copy; 91 | } 92 | 93 | 94 | var pageEvaluateDeprecatedFn = util.deprecate(function () {}, "Deprecated 'page.evaluate(fn, callback, args...)' syntax - use 'page.evaluate(fn, args..., callback)' instead"); 95 | var createDeprecatedFn = util.deprecate(function () {}, "Deprecated '.create(callback, options)' syntax - use '.create(options, callback)' instead"); 96 | var pageWaitForSelectorDeprecatedFn = util.deprecate(function () {}, "Deprecated 'page.waitForSelector(selector, callback, timeout)' syntax - use 'page.waitForSelector(selector, timeout, callback)' instead"); 97 | var phantomPathDeprecatedFn = util.deprecate(function () {}, "Deprecated 'phantomPath' option - use 'path' instead"); 98 | 99 | 100 | exports.create = function (options, callback) { 101 | if (callback && Object.prototype.toString.call(options) === '[object Function]') { 102 | createDeprecatedFn(); 103 | 104 | var tmp = options; 105 | 106 | options = callback; 107 | callback = tmp; 108 | } 109 | 110 | if (!callback) { 111 | callback = options; 112 | options = {}; 113 | } 114 | 115 | if (options.phantomPath) { 116 | phantomPathDeprecatedFn(); 117 | options.path = options.phantomPath; 118 | } 119 | 120 | if (!options.path) { 121 | options.path = 'phantomjs'; 122 | } 123 | 124 | if (typeof options.parameters === 'undefined') { options.parameters = {}; } 125 | 126 | function spawnPhantom(callback) { 127 | var args = []; 128 | 129 | if (Array.isArray(options.parameters)) { 130 | args = options.parameters; 131 | } else { 132 | Object.keys(options.parameters).forEach(function (parm) { 133 | args.push('--' + parm + '=' + options.parameters[parm]); 134 | }); 135 | } 136 | 137 | args = args.concat([ path.join(__dirname, 'bridge.js') ]); 138 | 139 | var phantom = spawn(options.path, args); 140 | 141 | phantom.once('error', function (err) { 142 | callback(err); 143 | }); 144 | 145 | phantom.stderr.on('data', function (data) { 146 | if (options.ignoreErrorPattern && options.ignoreErrorPattern.exec(data)) { 147 | return; 148 | } 149 | logger.error('' + data); 150 | }); 151 | 152 | var immediateExit = function (exitCode) { 153 | return callback(new HeadlessError('Phantom immediately exited with: ' + exitCode)); 154 | }; 155 | 156 | phantom.once('exit', immediateExit); 157 | 158 | // Wait for 'Ready' line 159 | phantom.stdout.once('data', function (data) { 160 | // setup normal listener now 161 | phantom.stdout.on('data', function (data) { 162 | logger.debug('' + data); 163 | }); 164 | 165 | var matches = data.toString().match(/Ready \[(\d+)\] \[(.*?)\]/); 166 | 167 | if (!matches) { 168 | phantom.kill(); 169 | callback(new HeadlessError('Unexpected output from PhantomJS: ' + data)); 170 | return; 171 | } 172 | 173 | phantom.removeListener('exit', immediateExit); 174 | 175 | var phantom_port = !matches[2] || matches[2].indexOf(':') === -1 ? (matches[2] || '0') : matches[2].split(':')[1]; 176 | 177 | phantom_port = parseInt(phantom_port, 0); 178 | 179 | if (phantom_port !== 0) { 180 | callback(null, phantom, phantom_port); 181 | return; 182 | } 183 | 184 | var phantom_pid = parseInt(matches[1], 0); 185 | 186 | // Now need to figure out what port it's listening on - since 187 | // Phantom is busted and can't tell us this we need to use lsof on mac, and netstat on Linux 188 | // Note that if phantom could tell you the port it ends up listening 189 | // on we wouldn't need to do this - server.port returns 0 when you ask 190 | // for port 0 (i.e. random free port). If they ever fix that this will 191 | // become much simpler 192 | var platform = require('os').platform(); 193 | var cmd = null; 194 | 195 | switch (platform) { 196 | case 'linux': 197 | // Modern distros usually have `iproute2` instead of `net-tools`. 198 | // Try `ss` first, then fallback to `netstat`. 199 | // 200 | // Note: 201 | // 202 | // - `grep "[,=]%d,"` contains variation, because `ss` output differs 203 | // between versions. 204 | // - `ss` can exist but fail in some env (#76). 205 | // 206 | cmd = 'ss -nlp | grep "[,=]%d," || netstat -nlp | grep "[[:space:]]%d/"'; 207 | break; 208 | 209 | case 'darwin': 210 | cmd = 'lsof -np %d | grep LISTEN'; 211 | break; 212 | 213 | case 'win32': 214 | cmd = 'netstat -ano | findstr /R "\\<%d\\>"'; 215 | break; 216 | 217 | case 'cygwin': 218 | cmd = 'netstat -ano | grep %d'; 219 | break; 220 | 221 | case 'freebsd': 222 | cmd = 'sockstat | grep %d'; 223 | break; 224 | 225 | default: 226 | phantom.kill(); 227 | callback(new HeadlessError('Your OS is not supported yet. Tell us how to get the listening port based on PID')); 228 | return; 229 | } 230 | 231 | // We do this twice - first to get ports this process is listening on 232 | // and again to get ports phantom is listening on. This is to work 233 | // around this bug in libuv: https://github.com/joyent/libuv/issues/962 234 | // - this is only necessary when using cluster, but it's here regardless 235 | var my_pid_command = cmd.replace(/%d/g, process.pid); 236 | 237 | exec(my_pid_command, function (err, stdout /*, stderr*/) { 238 | if (err !== null) { 239 | // This can happen if grep finds no matching lines, so ignore it. 240 | stdout = ''; 241 | } 242 | 243 | var re = /(?:127\.\d{1,3}\.\d{1,3}\.\d{1,3}|localhost):(\d+)/ig, match; 244 | var ports = []; 245 | 246 | while ((match = re.exec(stdout)) !== null) { 247 | ports.push(match[1]); 248 | } 249 | 250 | var phantom_pid_command = cmd.replace(/%d/g, phantom_pid); 251 | 252 | exec(phantom_pid_command, function (err, stdout /*, stderr*/) { 253 | if (err !== null) { 254 | phantom.kill(); 255 | callback(new HeadlessError('Error executing command to extract phantom ports: ' + err)); 256 | return; 257 | } 258 | 259 | var port; 260 | 261 | while ((match = re.exec(stdout)) !== null) { 262 | if (ports.indexOf(match[1]) === -1) { 263 | port = match[1]; 264 | } 265 | } 266 | 267 | if (!port) { 268 | phantom.kill(); 269 | callback(new HeadlessError('Error extracting port from: ' + stdout)); 270 | return; 271 | } 272 | 273 | callback(null, phantom, port); 274 | }); 275 | }); 276 | }); 277 | } 278 | 279 | spawnPhantom(function (err, phantom, port) { 280 | if (err) { 281 | callback(err); 282 | return; 283 | } 284 | 285 | var pages = {}; 286 | 287 | var setup_new_page = function (id) { 288 | var methods = [ 289 | 'addCookie', 'childFramesCount', 'childFramesName', 'clearCookies', 'close', 290 | 'currentFrameName', 'deleteCookie', 'evaluateJavaScript', 291 | 'evaluateAsync', 'getPage', 'go', 'goBack', 'goForward', 'includeJs', 292 | 'injectJs', 'open', 'openUrl', 'release', 'reload', 'render', 'renderBase64', 293 | 'sendEvent', 'setContent', 'stop', 'switchToFocusedFrame', 'switchToFrame', 294 | 'switchToFrame', 'switchToChildFrame', 'switchToChildFrame', 'switchToMainFrame', 295 | 'switchToParentFrame', 'uploadFile', 'clearMemoryCache' 296 | ]; 297 | 298 | var page = { 299 | setFn: function (name, fn, cb) { 300 | request_queue.push([ [ id, 'setFunction', name, fn.toString() ], callbackOrDummy(cb, poll_func) ]); 301 | }, 302 | 303 | get: function (name, cb) { 304 | request_queue.push([ [ id, 'getProperty', name ], callbackOrDummy(cb, poll_func) ]); 305 | }, 306 | 307 | set: function (name, val, cb) { 308 | // Special case for `paperSize.header.contents` property. 309 | // Property should be wrapped by `phantom.callback` in bridge. 310 | if (name === 'paperSize.header.contents' && val) { 311 | val = String(val); 312 | } else if (name === 'paperSize.header' && val.contents) { 313 | val = clone(val); 314 | val.contents = String(val.contents); 315 | } else if (name === 'paperSize' && val.header && val.header.contents) { 316 | val = clone(val); 317 | val.header.contents = String(val.header.contents); 318 | } 319 | 320 | // Special case for `paperSize.footer.contents` property. 321 | // Property should be wrapped by `phantom.callback` in bridge. 322 | if (name === 'paperSize.footer.contents' && val) { 323 | val = String(val); 324 | } else if (name === 'paperSize.footer' && val.contents) { 325 | val = clone(val); 326 | val.contents = String(val.contents); 327 | } else if (name === 'paperSize' && val.footer && val.footer.contents) { 328 | val = clone(val); 329 | val.footer.contents = String(val.footer.contents); 330 | } 331 | 332 | request_queue.push([ [ id, 'setProperty', name, val ], callbackOrDummy(cb, poll_func) ]); 333 | }, 334 | 335 | evaluate: function (fn, cb) { 336 | var extra_args = []; 337 | 338 | if (arguments.length > 2) { 339 | if (Object.prototype.toString.call(arguments[arguments.length - 1]) === '[object Function]') { 340 | extra_args = Array.prototype.slice.call(arguments, 1, -1); 341 | cb = arguments[arguments.length - 1]; 342 | } else { 343 | pageEvaluateDeprecatedFn(); 344 | extra_args = Array.prototype.slice.call(arguments, 2); 345 | } 346 | } 347 | 348 | request_queue.push([ [ id, 'evaluate', fn.toString() ].concat(extra_args), callbackOrDummy(cb, poll_func) ]); 349 | }, 350 | 351 | waitForSelector: function (selector, timeout, cb) { 352 | if (cb && Object.prototype.toString.call(timeout) === '[object Function]') { 353 | pageWaitForSelectorDeprecatedFn(); 354 | 355 | var tmp = cb; 356 | 357 | cb = timeout; 358 | timeout = tmp; 359 | } 360 | 361 | if (!cb) { 362 | cb = timeout; 363 | // Default timeout is 10 sec 364 | timeout = 10000; 365 | } 366 | 367 | var startTime = Date.now(); 368 | var timeoutInterval = 150; 369 | // if evaluate succeeds, invokes callback w/ true, if timeout, 370 | // invokes w/ false, otherwise just exits 371 | var testForSelector = function () { 372 | var elapsedTime = Date.now() - startTime; 373 | 374 | if (elapsedTime > timeout) { 375 | cb(new HeadlessError('Timeout waiting for selector: ' + selector)); 376 | return; 377 | } 378 | 379 | /*eslint-disable handle-callback-err*/ 380 | page.evaluate(function (selector) { 381 | return document.querySelectorAll(selector).length; 382 | }, selector, function (err, result) { 383 | if (result > 0) { // selector found 384 | cb(); 385 | } else { 386 | setTimeout(testForSelector, timeoutInterval); 387 | } 388 | }); 389 | }; 390 | 391 | setTimeout(testForSelector, timeoutInterval); 392 | } 393 | }; 394 | 395 | methods.forEach(function (method) { 396 | page[method] = function () { 397 | var all_args = Array.prototype.slice.call(arguments); 398 | var callback = null; 399 | 400 | if (all_args.length > 0 && typeof all_args[all_args.length - 1] === 'function') { 401 | callback = all_args.pop(); 402 | } 403 | 404 | var req_params = [ id, method ]; 405 | 406 | request_queue.push([ req_params.concat(all_args), callbackOrDummy(callback, poll_func) ]); 407 | }; 408 | }); 409 | 410 | pages[id] = page; 411 | 412 | return page; 413 | }; 414 | 415 | var poll_func = setup_long_poll(phantom, port, pages, setup_new_page); 416 | 417 | var request_queue = queue(function (paramarr, next) { 418 | var params = paramarr[0]; 419 | var callback = paramarr[1]; 420 | var page = params[0]; 421 | var method = params[1]; 422 | var args = params.slice(2); 423 | 424 | var http_opts = { 425 | hostname: 'localhost', 426 | port: port, 427 | path: '/', 428 | method: 'POST' 429 | }; 430 | 431 | phantom.POSTING = true; 432 | 433 | var req = http.request(http_opts, function (res) { 434 | var err = res.statusCode === 500 ? true : false; 435 | var data = ''; 436 | 437 | res.setEncoding('utf8'); 438 | 439 | res.on('data', function (chunk) { 440 | data += chunk; 441 | }); 442 | 443 | res.on('end', function () { 444 | phantom.POSTING = false; 445 | 446 | if (!data) { 447 | // If method is exit - response may be empty, because server could be stopped while sending 448 | if (method === 'exit') { 449 | next(); 450 | callback(); 451 | return; 452 | } 453 | 454 | next(); 455 | callback(new HeadlessError('No response body for page.' + method + '()')); 456 | return; 457 | } 458 | 459 | var results; 460 | 461 | try { 462 | results = JSON.parse(data).data; 463 | } catch (error) { 464 | // If method is exit - response may be broken, because server could be stopped while sending 465 | if (method === 'exit') { 466 | next(); 467 | callback(); 468 | return; 469 | } 470 | 471 | next(); 472 | callback(error); 473 | return; 474 | } 475 | 476 | if (err) { 477 | next(); 478 | callback(results); 479 | return; 480 | } 481 | 482 | if (method === 'createPage') { 483 | var id = results.page_id; 484 | var page = setup_new_page(id); 485 | 486 | next(); 487 | callback(null, page); 488 | return; 489 | } 490 | 491 | // Not createPage - just run the callback 492 | next(); 493 | callback(null, results); 494 | }); 495 | }); 496 | 497 | req.on('error', function (err) { 498 | // If phantom already killed by `exit` command - callback without error 499 | if (phantom.killed) { 500 | next(); 501 | callback(); 502 | return; 503 | } 504 | 505 | logger.warn('Request() error evaluating ' + method + '() call: ' + err); 506 | callback(new HeadlessError('Request() error evaluating ' + method + '() call: ' + err)); 507 | }); 508 | 509 | req.setHeader('Content-Type', 'application/json'); 510 | 511 | var json = JSON.stringify({ page: page, method: method, args: args }); 512 | 513 | req.setHeader('Content-Length', Buffer.byteLength(json)); 514 | req.write(json); 515 | req.end(); 516 | }); 517 | 518 | var proxy = { 519 | process: phantom, 520 | 521 | setProxy: function (ip, port, proxyType, user, password, callback) { 522 | request_queue.push([ [ 0, 'setProxy', ip, port, proxyType, user, password ], callbackOrDummy(callback, poll_func) ]); 523 | }, 524 | 525 | createPage: function (callback) { 526 | request_queue.push([ [ 0, 'createPage' ], callbackOrDummy(callback, poll_func) ]); 527 | }, 528 | 529 | injectJs: function (filename, callback) { 530 | request_queue.push([ [ 0, 'injectJs', filename ], callbackOrDummy(callback, poll_func) ]); 531 | }, 532 | 533 | addCookie: function (cookie, callback) { 534 | request_queue.push([ [ 0, 'addCookie', cookie ], callbackOrDummy(callback, poll_func) ]); 535 | }, 536 | 537 | clearCookies: function (callback) { 538 | request_queue.push([ [ 0, 'clearCookies' ], callbackOrDummy(callback, poll_func) ]); 539 | }, 540 | 541 | deleteCookie: function (cookie, callback) { 542 | request_queue.push([ [ 0, 'deleteCookie', cookie ], callbackOrDummy(callback, poll_func) ]); 543 | }, 544 | 545 | set : function (property, value, callback) { 546 | request_queue.push([ [ 0, 'setProperty', property, value ], callbackOrDummy(callback, poll_func) ]); 547 | }, 548 | 549 | get : function (property, callback) { 550 | request_queue.push([ [ 0, 'getProperty', property ], callbackOrDummy(callback, poll_func) ]); 551 | }, 552 | 553 | exit: function (callback) { 554 | phantom.kill('SIGTERM'); 555 | 556 | // In case of SlimerJS `kill` will close only wrapper of xulrunner. 557 | // We should send `exit` command to process. 558 | request_queue.push([ [ 0, 'exit', 0 ], callbackOrDummy(callback) ]); 559 | }, 560 | 561 | on: function () { 562 | phantom.on.apply(phantom, arguments); 563 | } 564 | }; 565 | 566 | callback(null, proxy); 567 | }); 568 | }; 569 | 570 | 571 | function setup_long_poll(phantom, port, pages, setup_new_page) { 572 | var http_opts = { 573 | hostname: 'localhost', 574 | port: port, 575 | path: '/', 576 | method: 'GET' 577 | }; 578 | 579 | var dead = false; 580 | phantom.once('exit', function () { dead = true; }); 581 | 582 | var poll_func = function (cb) { 583 | if (dead) { 584 | cb(new HeadlessError('Phantom Process died')); 585 | return; 586 | } 587 | 588 | if (phantom.POSTING) { 589 | cb(); 590 | return; 591 | } 592 | 593 | var req = http.get(http_opts, function (res) { 594 | res.setEncoding('utf8'); 595 | var data = ''; 596 | res.on('data', function (chunk) { 597 | data += chunk; 598 | }); 599 | res.on('end', function () { 600 | var results; 601 | 602 | if (dead) { 603 | cb(new HeadlessError('Phantom Process died')); 604 | return; 605 | } 606 | 607 | try { 608 | results = JSON.parse(data).data; 609 | } catch (err) { 610 | logger.warn('Error parsing JSON from phantom: ' + err); 611 | logger.warn('Data from phantom was: ' + data); 612 | cb(new HeadlessError('Error parsing JSON from phantom: ' + err 613 | + '\nData from phantom was: ' + data)); 614 | return; 615 | } 616 | 617 | results.forEach(function (r) { 618 | var new_page, callbackFunc, cb; 619 | 620 | if (r.page_id) { 621 | if (pages[r.page_id] && r.callback === 'onPageCreated') { 622 | new_page = setup_new_page(r.args[0]); 623 | 624 | if (pages[r.page_id].onPageCreated) { 625 | pages[r.page_id].onPageCreated(new_page); 626 | } 627 | 628 | } else if (pages[r.page_id] && pages[r.page_id][r.callback]) { 629 | callbackFunc = pages[r.page_id][r.callback]; 630 | 631 | if (callbackFunc.length > 1) { 632 | // We use `apply` if the function is expecting multiple args 633 | callbackFunc.apply(pages[r.page_id], wrapArray(r.args)); 634 | } else { 635 | // Old `call` behaviour is deprecated 636 | callbackFunc.call(pages[r.page_id], unwrapArray(r.args)); 637 | } 638 | } 639 | } else { 640 | cb = callbackOrDummy(phantom[r.callback]); 641 | cb.apply(phantom, r.args); 642 | } 643 | }); 644 | 645 | cb(); 646 | }); 647 | }); 648 | 649 | req.on('error', function (err) { 650 | if (dead || phantom.killed) { return; } 651 | 652 | if (err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED') { 653 | try { 654 | phantom.kill(); 655 | } catch (e) { 656 | // we don't care 657 | } 658 | dead = true; 659 | cb(new HeadlessError('Phantom Process died')); 660 | return; 661 | } 662 | 663 | logger.warn('Poll Request error: ' + err); 664 | }); 665 | }; 666 | 667 | var repeater = function () { 668 | // If phantom already killed - stop repeat timer 669 | if (dead || phantom.killed) { 670 | return; 671 | } 672 | 673 | setTimeout(function () { 674 | poll_func(repeater); 675 | }, POLL_INTERVAL); 676 | }; 677 | 678 | repeater(); 679 | 680 | return poll_func; 681 | } 682 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Matt Sergeant (matt@hubdoc.com)", 3 | "name": "node-phantom-simple", 4 | "description": "Simple and reliable bridge between Node.js and PhantomJS / SlimerJS", 5 | "version": "2.2.4", 6 | "license": "MIT", 7 | "repository": "baudehlo/node-phantom-simple", 8 | "main": "node-phantom-simple.js", 9 | "files": [ 10 | "bridge.js", 11 | "headless_error.js", 12 | "node-phantom-simple.js" 13 | ], 14 | "scripts": { 15 | "lint": "./node_modules/.bin/eslint .", 16 | "test": "npm run test-phantom && npm run test-slimer", 17 | "test-phantom": "npm run lint && ./node_modules/.bin/mocha", 18 | "test-slimer": "ENGINE=slimerjs ./node_modules/.bin/mocha" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^3.4.0", 22 | "mocha": "^3.0.2", 23 | "phantomjs": "^1.9.19" 24 | }, 25 | "dependencies": { 26 | "debug": "^2.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | globals: 2 | describe: false 3 | it: false 4 | before: false 5 | after: false 6 | window: true 7 | document: true 8 | -------------------------------------------------------------------------------- /test/fixtures/injecttest.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console, strict*/ 2 | console.log('injected'); 3 | -------------------------------------------------------------------------------- /test/fixtures/modifytest.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable strict*/ 2 | document.getElementsByTagName('h1')[0].innerText = 'Hello Test'; 3 | -------------------------------------------------------------------------------- /test/fixtures/uploadtest.txt: -------------------------------------------------------------------------------- 1 | Hello World -------------------------------------------------------------------------------- /test/fixtures/verifyrender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudehlo/node-phantom-simple/ad6a143375df569e280349db409814e80c0e095d/test/fixtures/verifyrender.png -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | 8 | // Generate path in os tmp dir 9 | function tmp() { 10 | return path.join( 11 | require('os').tmpdir(), 12 | require('crypto').randomBytes(8).toString('hex') + '.png' 13 | ); 14 | } 15 | 16 | 17 | // Copy file to tmp dir & return new name 18 | function toTmp(filePath) { 19 | var p = tmp(); 20 | 21 | fs.writeFileSync(p, fs.readFileSync(filePath)); 22 | 23 | return p; 24 | } 25 | 26 | 27 | // Delete file if exists 28 | function unlink(filePath) { 29 | if (fs.existsSync(filePath)) { 30 | fs.unlinkSync(filePath); 31 | } 32 | } 33 | 34 | 35 | exports.tmp = tmp; 36 | exports.toTmp = toTmp; 37 | exports.unlink = unlink; 38 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec -t 60000 2 | -------------------------------------------------------------------------------- /test/test_engine_bad_path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('bad path', function () { 9 | it('bad path produces an error', function (done) { 10 | driver.create({ path: '@@@', ignoreErrorPattern: /execvp/ }, function (err) { 11 | assert.notEqual(null, err); 12 | done(); 13 | }); 14 | }); 15 | 16 | 17 | it('deprecated path name still should be ok', function (done) { 18 | driver.create({ phantomPath: '@@@', ignoreErrorPattern: /execvp/ }, function (err) { 19 | assert.notEqual(null, err, 'Bad path produces an error'); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/test_engine_command_line_options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('command line options', function () { 9 | it('load-images is default', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.get('defaultPageSettings.loadImages', function (err, loadImages) { 17 | if (err) { 18 | done(err); 19 | return; 20 | } 21 | 22 | assert.equal(loadImages, true); 23 | 24 | browser.exit(done); 25 | }); 26 | }); 27 | }); 28 | 29 | 30 | it('load-images is true', function (done) { 31 | driver.create( 32 | { parameters: { 'load-images': true }, path: require(process.env.ENGINE || 'phantomjs').path }, 33 | function (err, browser) { 34 | if (err) { 35 | done(err); 36 | return; 37 | } 38 | 39 | browser.get('defaultPageSettings.loadImages', function (err, loadImages) { 40 | if (err) { 41 | done(err); 42 | return; 43 | } 44 | 45 | assert.equal(loadImages, true); 46 | 47 | browser.exit(done); 48 | }); 49 | } 50 | ); 51 | }); 52 | 53 | 54 | it('load-images is false', function (done) { 55 | driver.create({ parameters: { 'load-images': false }, path: require(process.env.ENGINE || 'phantomjs').path }, 56 | function (err, browser) { 57 | if (err) { 58 | done(err); 59 | return; 60 | } 61 | 62 | browser.get('defaultPageSettings.loadImages', function (err, loadImages) { 63 | if (err) { 64 | done(err); 65 | return; 66 | } 67 | 68 | assert.equal(loadImages, false); 69 | 70 | browser.exit(done); 71 | }); 72 | } 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/test_engine_create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var driver = require('../'); 5 | 6 | 7 | describe('engine.create()', function () { 8 | 9 | it('without options', function (done) { 10 | driver.create(function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.exit(done); 17 | }); 18 | }); 19 | 20 | 21 | it('callback is last', function (done) { 22 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | browser.exit(done); 29 | }); 30 | }); 31 | 32 | 33 | it('callback is first (legacy style)', function (done) { 34 | driver.create(function (err, browser) { 35 | if (err) { 36 | done(err); 37 | return; 38 | } 39 | 40 | browser.exit(done); 41 | }, { path: require(process.env.ENGINE || 'phantomjs').path }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/test_engine_get_hierarchical.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('page', function () { 9 | it('set get hierarchical', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.get('defaultPageSettings', function (err, defaultPageSettings) { 17 | if (err) { 18 | done(err); 19 | return; 20 | } 21 | 22 | browser.get('defaultPageSettings.userAgent', function (err, userAgent) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | assert.equal(userAgent, defaultPageSettings.userAgent); 29 | 30 | browser.exit(done); 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/test_engine_injectjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var helpers = require('./helpers'); 7 | var driver = require('../'); 8 | 9 | 10 | describe('engine', function () { 11 | it('injectjs', function (done) { 12 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 13 | if (err) { 14 | done(err); 15 | return; 16 | } 17 | 18 | var filePath = helpers.toTmp(path.join(__dirname, 'fixtures', 'injecttest.js')); 19 | 20 | browser.injectJs(filePath, function (err, result) { 21 | helpers.unlink(filePath); 22 | 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | assert.ok(result); 29 | 30 | browser.exit(done); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should return false on non-existent file', function (done) { 36 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 37 | if (err) { 38 | done(err); 39 | return; 40 | } 41 | 42 | var filePath = '/not/existent'; 43 | 44 | browser.injectJs(filePath, function (err, result) { 45 | 46 | if (err) { 47 | done(err); 48 | return; 49 | } 50 | assert.equal(result, false); 51 | browser.exit(done); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/test_engine_unexpected_exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('engine', function () { 9 | it('unexpected exit', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.createPage(function (err, page) { 17 | if (err) { 18 | done(err); 19 | return; 20 | } 21 | 22 | browser.exit(); // exit the phantom process at a strange time 23 | 24 | page.open('http://www.google.com', function (err) { 25 | assert.ok(!!err); // we expect an error 26 | done(); 27 | }); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/test_page_create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var driver = require('../'); 5 | 6 | 7 | describe('page', function () { 8 | it('create', function (done) { 9 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 10 | if (err) { 11 | done(err); 12 | return; 13 | } 14 | 15 | browser.createPage(function (err) { 16 | if (err) { 17 | done(err); 18 | return; 19 | } 20 | 21 | browser.exit(done); 22 | }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test_page_evaluate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page.evaluate()', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('

Hello World

'); 16 | }).listen(done); 17 | }); 18 | 19 | 20 | it('should return false as boolean (#43)', function (done) { 21 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 22 | if (err) { 23 | done(err); 24 | return; 25 | } 26 | 27 | browser.createPage(function (err, page) { 28 | if (err) { 29 | done(err); 30 | return; 31 | } 32 | 33 | page.open('http://localhost:' + server.address().port, function (err, status) { 34 | if (err) { 35 | done(err); 36 | return; 37 | } 38 | 39 | assert.equal(status, 'success'); 40 | 41 | // Engines are buggy and can corrupt result values: 42 | // 43 | // - SlimerJS 44 | // - undefined -> null 45 | // - PhantomJS 46 | // - undefined -> null 47 | // - null -> empty string 48 | // - [ 1, undefined, 2 ] -> null 49 | // 50 | page.evaluate(function () { 51 | return false; 52 | }, function (err, result) { 53 | if (err) { 54 | done(err); 55 | return; 56 | } 57 | 58 | assert.strictEqual(result, false); 59 | 60 | browser.exit(done); 61 | }); 62 | }); 63 | }); 64 | }); 65 | }); 66 | 67 | 68 | it('without extra args', function (done) { 69 | driver.create( 70 | { path: require(process.env.ENGINE || 'phantomjs').path, ignoreErrorPattern: /CoreText performance note/ }, 71 | function (err, browser) { 72 | if (err) { 73 | done(err); 74 | return; 75 | } 76 | 77 | browser.createPage(function (err, page) { 78 | if (err) { 79 | done(err); 80 | return; 81 | } 82 | 83 | page.open('http://localhost:' + server.address().port, function (err, status) { 84 | if (err) { 85 | done(err); 86 | return; 87 | } 88 | 89 | assert.equal(status, 'success'); 90 | 91 | page.evaluate(function () { 92 | return { h1text: document.getElementsByTagName('h1')[0].innerHTML }; 93 | }, function (err, result) { 94 | if (err) { 95 | done(err); 96 | return; 97 | } 98 | 99 | assert.equal(result.h1text, 'Hello World'); 100 | 101 | browser.exit(done); 102 | }); 103 | }); 104 | }); 105 | } 106 | ); 107 | }); 108 | 109 | 110 | it('with extra args, callback is last', function (done) { 111 | driver.create( 112 | { path: require(process.env.ENGINE || 'phantomjs').path, ignoreErrorPattern: /CoreText performance note/ }, 113 | function (err, browser) { 114 | if (err) { 115 | done(err); 116 | return; 117 | } 118 | 119 | browser.createPage(function (err, page) { 120 | if (err) { 121 | done(err); 122 | return; 123 | } 124 | 125 | page.open('http://localhost:' + server.address().port, function (err, status) { 126 | if (err) { 127 | done(err); 128 | return; 129 | } 130 | 131 | assert.equal(status, 'success'); 132 | 133 | page.evaluate(function (a, b, c) { 134 | return { h1text: document.getElementsByTagName('h1')[0].innerHTML, abc: a + b + c }; 135 | }, 'a', 'b', 'c', function (err, result) { 136 | if (err) { 137 | done(err); 138 | return; 139 | } 140 | 141 | assert.equal(result.h1text, 'Hello World'); 142 | assert.equal(result.abc, 'abc'); 143 | 144 | browser.exit(done); 145 | }); 146 | }); 147 | }); 148 | } 149 | ); 150 | }); 151 | 152 | 153 | it('with extra args (legacy style)', function (done) { 154 | driver.create( 155 | { path: require(process.env.ENGINE || 'phantomjs').path, ignoreErrorPattern: /CoreText performance note/ }, 156 | function (err, browser) { 157 | if (err) { 158 | done(err); 159 | return; 160 | } 161 | 162 | browser.createPage(function (err, page) { 163 | if (err) { 164 | done(err); 165 | return; 166 | } 167 | 168 | page.open('http://localhost:' + server.address().port, function (err, status) { 169 | if (err) { 170 | done(err); 171 | return; 172 | } 173 | 174 | assert.equal(status, 'success'); 175 | 176 | page.evaluate(function (a, b, c) { 177 | return { h1text: document.getElementsByTagName('h1')[0].innerHTML, abc: a + b + c }; 178 | }, function (err, result) { 179 | if (err) { 180 | done(err); 181 | return; 182 | } 183 | 184 | assert.equal(result.h1text, 'Hello World'); 185 | assert.equal(result.abc, 'abc'); 186 | 187 | browser.exit(done); 188 | }, 'a', 'b', 'c'); 189 | }); 190 | }); 191 | } 192 | ); 193 | }); 194 | 195 | 196 | after(function (done) { 197 | server.close(done); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/test_page_include_js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | if (request.url === '/test.js') { 15 | response.writeHead(200, { 'Content-Type': 'text/javascript' }); 16 | response.end('document.getElementsByTagName("h1")[0].innerText="Hello Test";'); 17 | } else { 18 | response.writeHead(200, { 'Content-Type': 'text/html' }); 19 | response.end('

Hello World

'); 20 | } 21 | }).listen(done); 22 | }); 23 | 24 | it('includeJs', function (done) { 25 | driver.create( 26 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 27 | function (err, browser) { 28 | if (err) { 29 | done(err); 30 | return; 31 | } 32 | 33 | browser.createPage(function (err, page) { 34 | if (err) { 35 | done(err); 36 | return; 37 | } 38 | 39 | page.open('http://localhost:' + server.address().port, function (err, status) { 40 | if (err) { 41 | done(err); 42 | return; 43 | } 44 | 45 | assert.equal(status, 'success', 'Status is success'); 46 | 47 | page.includeJs('http://localhost:' + server.address().port + '/test.js', function (err) { 48 | if (err) { 49 | done(err); 50 | return; 51 | } 52 | 53 | page.evaluate(function () { 54 | return [ document.getElementsByTagName('h1')[0].innerText, document.getElementsByTagName('script').length ]; 55 | }, function (err, result) { 56 | if (err) { 57 | done(err); 58 | return; 59 | } 60 | 61 | assert.equal(result[0], 'Hello Test', 'Script was executed'); 62 | assert.equal(result[1], 1, 'Added a new script tag'); 63 | 64 | browser.exit(done); 65 | }); 66 | }); 67 | }); 68 | }); 69 | } 70 | ); 71 | }); 72 | 73 | after(function (done) { 74 | server.close(done); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/test_page_open.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('Hello World'); 16 | }).listen(done); 17 | }); 18 | 19 | it('open', function (done) { 20 | driver.create( 21 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 22 | function (err, browser) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | browser.createPage(function (err, page) { 29 | if (err) { 30 | done(err); 31 | return; 32 | } 33 | 34 | page.open('http://localhost:' + server.address().port, function (err, status) { 35 | if (err) { 36 | done(err); 37 | return; 38 | } 39 | 40 | assert.equal(status, 'success'); 41 | 42 | browser.exit(done); 43 | }); 44 | }); 45 | } 46 | ); 47 | }); 48 | 49 | after(function (done) { 50 | server.close(done); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/test_page_push_notifications.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('push notifications', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('

Hello World

'); 16 | }).listen(done); 17 | }); 18 | 19 | 20 | it('onConsoleMessage', function (done) { 21 | driver.create(function (err, browser) { 22 | if (err) { 23 | done(err); 24 | return; 25 | } 26 | 27 | 28 | browser.createPage(function (err, page) { 29 | if (err) { 30 | done(err); 31 | return; 32 | } 33 | 34 | 35 | page.onConsoleMessage = function (msg) { 36 | assert.ok(/Test console message/.test(String(msg))); 37 | 38 | browser.exit(done); 39 | }; 40 | 41 | page.evaluate(function () { 42 | /*eslint-disable no-console*/ 43 | console.log('Test console message'); 44 | }, function (err) { 45 | if (err) { 46 | done(err); 47 | return; 48 | } 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | 55 | it('onCallback', function (done) { 56 | var url = 'http://localhost:' + server.address().port + '/'; 57 | 58 | driver.create(function (err, browser) { 59 | if (err) { 60 | done(err); 61 | return; 62 | } 63 | 64 | 65 | browser.createPage(function (err, page) { 66 | if (err) { 67 | done(err); 68 | return; 69 | } 70 | 71 | 72 | page.onCallback = function (msg) { 73 | assert.deepEqual(msg, { msg: 'callPhantom' }); 74 | 75 | browser.exit(done); 76 | }; 77 | 78 | page.open(url, function (err, status) { 79 | if (err) { 80 | done(err); 81 | return; 82 | } 83 | 84 | assert.equal(status, 'success'); 85 | }); 86 | }); 87 | }); 88 | }); 89 | 90 | 91 | it('onError', function (done) { 92 | var url = 'http://localhost:' + server.address().port + '/'; 93 | 94 | driver.create(function (err, browser) { 95 | if (err) { 96 | done(err); 97 | return; 98 | } 99 | 100 | 101 | browser.createPage(function (err, page) { 102 | if (err) { 103 | done(err); 104 | return; 105 | } 106 | 107 | 108 | page.onError = function (msg) { 109 | assert.ok(/conXsole/.test(String(msg))); 110 | 111 | browser.exit(done); 112 | }; 113 | 114 | page.open(url, function (err, status) { 115 | if (err) { 116 | done(err); 117 | return; 118 | } 119 | 120 | assert.equal(status, 'success'); 121 | }); 122 | }); 123 | }); 124 | }); 125 | 126 | 127 | it('onUrlChanged', function (done) { 128 | var url = 'http://localhost:' + server.address().port + '/'; 129 | 130 | driver.create(function (err, browser) { 131 | if (err) { 132 | done(err); 133 | return; 134 | } 135 | 136 | 137 | browser.createPage(function (err, page) { 138 | if (err) { 139 | done(err); 140 | return; 141 | } 142 | 143 | 144 | page.onUrlChanged = function (newUrl) { 145 | assert.equal(newUrl, url); 146 | 147 | browser.exit(done); 148 | }; 149 | 150 | page.open(url, function (err, status) { 151 | if (err) { 152 | done(err); 153 | return; 154 | } 155 | 156 | assert.equal(status, 'success'); 157 | }); 158 | }); 159 | }); 160 | }); 161 | 162 | 163 | it('onLoadStarted', function (done) { 164 | var url = 'http://localhost:' + server.address().port + '/'; 165 | 166 | driver.create(function (err, browser) { 167 | if (err) { 168 | done(err); 169 | return; 170 | } 171 | 172 | 173 | browser.createPage(function (err, page) { 174 | if (err) { 175 | done(err); 176 | return; 177 | } 178 | 179 | page.onLoadStarted = function () { 180 | browser.exit(done); 181 | }; 182 | 183 | page.open(url, function (err, status) { 184 | if (err) { 185 | done(err); 186 | return; 187 | } 188 | 189 | assert.equal(status, 'success'); 190 | }); 191 | }); 192 | }); 193 | }); 194 | 195 | 196 | it('onLoadFinished', function (done) { 197 | var url = 'http://localhost:' + server.address().port + '/'; 198 | 199 | driver.create(function (err, browser) { 200 | if (err) { 201 | done(err); 202 | return; 203 | } 204 | 205 | 206 | browser.createPage(function (err, page) { 207 | if (err) { 208 | done(err); 209 | return; 210 | } 211 | 212 | page.onLoadFinished = function () { 213 | browser.exit(done); 214 | }; 215 | 216 | page.open(url, function (err, status) { 217 | if (err) { 218 | done(err); 219 | return; 220 | } 221 | 222 | assert.equal(status, 'success'); 223 | }); 224 | }); 225 | }); 226 | }); 227 | 228 | 229 | it('onResourceReceived', function (done) { 230 | var url = 'http://localhost:' + server.address().port + '/'; 231 | 232 | driver.create(function (err, browser) { 233 | if (err) { 234 | done(err); 235 | return; 236 | } 237 | 238 | 239 | browser.createPage(function (err, page) { 240 | if (err) { 241 | done(err); 242 | return; 243 | } 244 | 245 | page.onResourceReceived = function (res) { 246 | if (res.stage === 'end') { 247 | browser.exit(done); 248 | } 249 | }; 250 | 251 | page.open(url, function (err, status) { 252 | if (err) { 253 | done(err); 254 | return; 255 | } 256 | 257 | assert.equal(status, 'success'); 258 | }); 259 | }); 260 | }); 261 | }); 262 | 263 | 264 | after(function (done) { 265 | server.close(done); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /test/test_page_release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('page', function () { 9 | it('release', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.createPage(function (err, page) { 17 | assert.ifError(err); 18 | page.close(function (err) { 19 | assert.ifError(err); 20 | 21 | browser.exit(done); 22 | }); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/test_page_render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var fs = require('fs'); 6 | var assert = require('assert'); 7 | var helpers = require('./helpers'); 8 | var driver = require('../'); 9 | 10 | 11 | describe('page', function () { 12 | var server; 13 | var testFileName = helpers.tmp(); 14 | 15 | 16 | before(function (done) { 17 | server = http.createServer(function (request, response) { 18 | response.writeHead(200, { 'Content-Type': 'text/html' }); 19 | response.end('Hello World'); 20 | }).listen(done); 21 | }); 22 | 23 | 24 | it('render to binary & base64', function (done) { 25 | driver.create( 26 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 27 | function (err, browser) { 28 | if (err) { 29 | done(err); 30 | return; 31 | } 32 | 33 | browser.createPage(function (err, page) { 34 | if (err) { 35 | done(err); 36 | return; 37 | } 38 | 39 | page.open('http://localhost:' + server.address().port, function (err, status) { 40 | if (err) { 41 | done(err); 42 | return; 43 | } 44 | 45 | assert.equal(status, 'success'); 46 | 47 | page.render(testFileName, function (err) { 48 | if (err) { 49 | done(err); 50 | return; 51 | } 52 | 53 | var stat = fs.statSync(testFileName); 54 | 55 | // Relaxed check to work in any browser/OS 56 | // We should have image and this image should be > 0 bytes. 57 | assert.ok(stat.size > 100, 'generated image too small'); 58 | 59 | 60 | page.renderBase64('png', function (err, imagedata) { 61 | if (err) { 62 | done(err); 63 | return; 64 | } 65 | 66 | // Base64 decoded image should be the same (check size only) 67 | assert.equal((new Buffer(imagedata, 'base64')).length, stat.size); 68 | 69 | browser.exit(done); 70 | }); 71 | }); 72 | }); 73 | }); 74 | } 75 | ); 76 | }); 77 | 78 | 79 | after(function (done) { 80 | helpers.unlink(testFileName); 81 | server.close(done); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/test_page_send_event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('

Hello World

'); 16 | }).listen(done); 17 | }); 18 | 19 | it('send event', function (done) { 20 | driver.create( 21 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 22 | function (err, browser) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | browser.createPage(function (err, page) { 29 | if (err) { 30 | done(err); 31 | return; 32 | } 33 | 34 | page.open('http://localhost:' + server.address().port, function (err, status) { 35 | if (err) { 36 | done(err); 37 | return; 38 | } 39 | 40 | assert.equal(status, 'success'); 41 | page.sendEvent('click', 30, 20, function (err) { 42 | if (err) { 43 | done(err); 44 | return; 45 | } 46 | 47 | page.evaluate(function () { 48 | return document.getElementsByTagName('h1')[0].innerText; 49 | }, function (err, result) { 50 | if (err) { 51 | done(err); 52 | return; 53 | } 54 | 55 | assert.equal(result, 'Hello Test'); 56 | 57 | browser.exit(done); 58 | }); 59 | }); 60 | }); 61 | }); 62 | } 63 | ); 64 | }); 65 | 66 | after(function (done) { 67 | server.close(done); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/test_page_set_fn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('

Hello World

'); 16 | }).listen(done); 17 | }); 18 | 19 | it('setFn', function (done) { 20 | var url = 'http://localhost:' + server.address().port + '/'; 21 | 22 | driver.create( 23 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 24 | function (err, browser) { 25 | if (err) { 26 | done(err); 27 | return; 28 | } 29 | 30 | browser.createPage(function (err, page) { 31 | if (err) { 32 | done(err); 33 | return; 34 | } 35 | 36 | /*eslint-disable no-undefined, no-undef-init*/ 37 | 38 | var messageForwardedByOnConsoleMessage = undefined; 39 | var localMsg = undefined; 40 | 41 | page.onConsoleMessage = function (msg) { 42 | messageForwardedByOnConsoleMessage = msg; 43 | }; 44 | 45 | page.setFn('onCallback', function (msg) { 46 | localMsg = msg; 47 | page.onConsoleMessage(msg); 48 | }); 49 | 50 | page.open(url, function (err) { 51 | if (err) { 52 | done(err); 53 | return; 54 | } 55 | 56 | assert.ok(localMsg === undefined); 57 | assert.ok(/handled on phantom-side/.test(String(messageForwardedByOnConsoleMessage))); 58 | 59 | browser.exit(done); 60 | }); 61 | }); 62 | } 63 | ); 64 | }); 65 | 66 | after(function (done) { 67 | server.close(done); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/test_page_set_get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('page', function () { 9 | it('set get', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.createPage(function (err, page) { 17 | if (err) { 18 | done(err); 19 | return; 20 | } 21 | 22 | page.get('viewportSize', function (err, oldValue) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | page.set('viewportSize', { width: 800, height: 600 }, function (err) { 29 | if (err) { 30 | done(err); 31 | return; 32 | } 33 | 34 | page.get('viewportSize', function (err, newValue) { 35 | if (err) { 36 | done(err); 37 | return; 38 | } 39 | 40 | assert.notEqual(oldValue, newValue); 41 | 42 | var rnd = Math.floor(100000 * Math.random()); 43 | 44 | page.set('zoomFactor', rnd, function (err) { 45 | if (err) { 46 | done(err); 47 | return; 48 | } 49 | 50 | page.get('zoomFactor', function (err, zoomValue) { 51 | if (err) { 52 | done(err); 53 | return; 54 | } 55 | 56 | assert.equal(zoomValue, rnd); 57 | 58 | page.get('settings', function (err, oldSettings) { 59 | if (err) { 60 | done(err); 61 | return; 62 | } 63 | 64 | page.set('settings', { userAgent: 'node-phantom tester' }, function (err) { 65 | if (err) { 66 | done(err); 67 | return; 68 | } 69 | 70 | page.get('settings', function (err, newSettings) { 71 | if (err) { 72 | done(err); 73 | return; 74 | } 75 | 76 | assert.notEqual(oldSettings.userAgent, newSettings.userAgent); 77 | 78 | browser.exit(done); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/test_page_set_get_hierarchical.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('assert'); 5 | var driver = require('../'); 6 | 7 | 8 | describe('page', function () { 9 | it('set get hierarchical', function (done) { 10 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | 16 | browser.createPage(function (err, page) { 17 | if (err) { 18 | done(err); 19 | return; 20 | } 21 | 22 | page.set('settings.userAgent', 'node-phantom tester', function (err) { 23 | if (err) { 24 | done(err); 25 | return; 26 | } 27 | 28 | page.get('settings.userAgent', function (err, ua) { 29 | if (err) { 30 | done(err); 31 | return; 32 | } 33 | 34 | assert.equal(ua, 'node-phantom tester'); 35 | 36 | page.get('viewportSize.width', function (err, oldValue) { 37 | if (err) { 38 | done(err); 39 | return; 40 | } 41 | 42 | page.set('viewportSize.width', 3000, function (err) { 43 | if (err) { 44 | done(err); 45 | return; 46 | } 47 | 48 | page.get('viewportSize.width', function (err, newValue) { 49 | if (err) { 50 | done(err); 51 | return; 52 | } 53 | 54 | assert.notEqual(oldValue, newValue); 55 | assert.equal(newValue, 3000); 56 | 57 | browser.exit(done); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/test_page_upload_file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var path = require('path'); 6 | var assert = require('assert'); 7 | var helpers = require('./helpers'); 8 | var driver = require('../'); 9 | 10 | 11 | describe('page', function () { 12 | var server; 13 | var gotFile = false; 14 | 15 | before(function (done) { 16 | server = http.createServer(function (request, response) { 17 | if (request.url === '/upload') { 18 | request.on('data', function (buffer) { 19 | gotFile = buffer.toString('ascii').indexOf('Hello World') > 0; 20 | }); 21 | } else { 22 | response.writeHead(200, { 'Content-Type': 'text/html' }); 23 | response.end('
'); 24 | } 25 | }).listen(done); 26 | }); 27 | 28 | it('uploadFile', function (done) { 29 | driver.create( 30 | { ignoreErrorPattern: /CoreText performance note/, path: require(process.env.ENGINE || 'phantomjs').path }, 31 | function (err, browser) { 32 | if (err) { 33 | done(err); 34 | return; 35 | } 36 | 37 | browser.createPage(function (err, page) { 38 | if (err) { 39 | done(err); 40 | return; 41 | } 42 | 43 | page.open('http://localhost:' + server.address().port, function (err, status) { 44 | if (err) { 45 | done(err); 46 | return; 47 | } 48 | 49 | assert.equal(status, 'success'); 50 | 51 | var filePath = helpers.toTmp(path.join(__dirname, 'fixtures', 'uploadtest.txt')); 52 | 53 | page.uploadFile('input[name=test]', filePath, function (err) { 54 | if (err) { 55 | helpers.unlink(filePath); 56 | done(err); 57 | return; 58 | } 59 | 60 | page.evaluate(function () { 61 | document.forms.testform.submit(); 62 | }, function (err) { 63 | if (err) { 64 | helpers.unlink(filePath); 65 | done(err); 66 | return; 67 | } 68 | 69 | setTimeout(function () { 70 | assert.ok(gotFile); 71 | 72 | helpers.unlink(filePath); 73 | browser.exit(done); 74 | }, 100); 75 | }); 76 | }); 77 | }); 78 | }); 79 | } 80 | ); 81 | }); 82 | 83 | after(function (done) { 84 | server.close(done); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/test_page_wait_for_selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var http = require('http'); 5 | var assert = require('assert'); 6 | var driver = require('../'); 7 | 8 | 9 | describe('page.waitForSelector()', function () { 10 | var server; 11 | 12 | before(function (done) { 13 | server = http.createServer(function (request, response) { 14 | response.writeHead(200, { 'Content-Type': 'text/html' }); 15 | response.end('' + 16 | '' + 21 | ''); 22 | }).listen(done); 23 | }); 24 | 25 | 26 | it('callback is last', function (done) { 27 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 28 | if (err) { 29 | done(err); 30 | return; 31 | } 32 | 33 | browser.createPage(function (err, page) { 34 | if (err) { 35 | done(err); 36 | return; 37 | } 38 | 39 | page.open('http://localhost:' + server.address().port, function (err, status) { 40 | if (err) { 41 | done(err); 42 | return; 43 | } 44 | 45 | assert.equal(status, 'success'); 46 | 47 | page.waitForSelector('#test', 2000, function (err) { 48 | if (err) { 49 | done(err); 50 | return; 51 | } 52 | 53 | browser.exit(done); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | 61 | it('callback without timeout', function (done) { 62 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 63 | if (err) { 64 | done(err); 65 | return; 66 | } 67 | 68 | browser.createPage(function (err, page) { 69 | if (err) { 70 | done(err); 71 | return; 72 | } 73 | 74 | page.open('http://localhost:' + server.address().port, function (err, status) { 75 | if (err) { 76 | done(err); 77 | return; 78 | } 79 | 80 | assert.equal(status, 'success'); 81 | 82 | page.waitForSelector('#test', function (err) { 83 | if (err) { 84 | done(err); 85 | return; 86 | } 87 | 88 | browser.exit(done); 89 | }); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | 96 | it('callback before timeout (legacy style)', function (done) { 97 | driver.create({ path: require(process.env.ENGINE || 'phantomjs').path }, function (err, browser) { 98 | if (err) { 99 | done(err); 100 | return; 101 | } 102 | 103 | browser.createPage(function (err, page) { 104 | if (err) { 105 | done(err); 106 | return; 107 | } 108 | 109 | page.open('http://localhost:' + server.address().port, function (err, status) { 110 | if (err) { 111 | done(err); 112 | return; 113 | } 114 | 115 | assert.equal(status, 'success'); 116 | 117 | page.waitForSelector('#test', function (err) { 118 | if (err) { 119 | done(err); 120 | return; 121 | } 122 | 123 | browser.exit(done); 124 | }, 2000); 125 | }); 126 | }); 127 | }); 128 | }); 129 | 130 | 131 | after(function (done) { 132 | server.close(done); 133 | }); 134 | }); 135 | --------------------------------------------------------------------------------