├── test
├── mocha.opts
├── fixtures
│ ├── uploadtest.txt
│ ├── injecttest.js
│ ├── modifytest.js
│ └── verifyrender.png
├── .eslintrc
├── test_page_create.js
├── test_page_release.js
├── test_engine_bad_path.js
├── helpers.js
├── test_engine_unexpected_exit.js
├── test_engine_get_hierarchical.js
├── test_engine_create.js
├── test_page_open.js
├── test_engine_injectjs.js
├── test_page_set_get_hierarchical.js
├── test_engine_command_line_options.js
├── test_page_set_fn.js
├── test_page_send_event.js
├── test_page_include_js.js
├── test_page_render.js
├── test_page_set_get.js
├── test_page_upload_file.js
├── test_page_wait_for_selector.js
├── test_page_evaluate.js
└── test_page_push_notifications.js
├── .gitignore
├── .travis.yml
├── headless_error.js
├── package.json
├── LICENSE
├── CHANGELOG.md
├── .eslintrc.yml
├── README.md
├── bridge.js
└── node-phantom-simple.js
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | -R spec -t 60000
2 |
--------------------------------------------------------------------------------
/test/fixtures/uploadtest.txt:
--------------------------------------------------------------------------------
1 | Hello World
--------------------------------------------------------------------------------
/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/verifyrender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baudehlo/node-phantom-simple/HEAD/test/fixtures/verifyrender.png
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | globals:
2 | describe: false
3 | it: false
4 | before: false
5 | after: false
6 | window: true
7 | document: true
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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_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_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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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_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_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_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_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_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_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_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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node-phantom-simple
2 | ===================
3 |
4 | [](https://travis-ci.org/baudehlo/node-phantom-simple)
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------