├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── bin └── phantom.js ├── decls ├── config.js └── logger.js ├── docs ├── .nojekyll ├── README.md ├── _media │ ├── example.html │ └── example.md ├── _navbar.md ├── _sidebar.md ├── advanced.md ├── index.html ├── install.md ├── migrating.md ├── page_obj.md ├── phantom_obj.md ├── update_log.md ├── work.md └── zh-cn │ ├── README.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── advanced.md │ ├── install.md │ ├── migrating.md │ ├── page_obj.md │ ├── phantom_obj.md │ ├── update_log.md │ └── work.md ├── examples ├── .babelrc ├── .gitignore ├── Dockerfile ├── async.js ├── cluster.js ├── co.js ├── out_obj.js ├── package.json ├── render.js └── simple.js ├── package-lock.json ├── package.json └── src ├── __mocks__ └── child_process.js ├── __tests__ ├── .babelrc ├── command.test.js ├── index.test.js ├── inject_example.js ├── out_object.test.js ├── page.evaluate.test.js ├── page.event.test.js ├── page.test.js └── phantom.test.js ├── command.js ├── index.js ├── out_object.js ├── page.js ├── phantom.js └── shim ├── .babelrc └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": 8 6 | } 7 | }], 8 | "flow" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | examples 4 | node_modules 5 | flow-typed 6 | coverage 7 | decls 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | jest: true, 8 | phantomjs: true, 9 | }, 10 | extends: ['airbnb-base', 'plugin:flowtype/recommended'], 11 | parser: 'babel-eslint', 12 | plugins: ['import', 'flowtype'], 13 | }; 14 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | decls/ 3 | 4 | 5 | [options] 6 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | This page describes how to contribute to `phantom` npm package. 6 | 7 | Do not create a pull request or issue without reading this first. 8 | 9 | ## Issues 10 | Do not create issues to ask questions. Issues are not a question and answer forum. Issue are to be created when expected outcome is different than actual. Provide example code, expected behavior and actual behavior. 11 | 12 | Explain the problem and include additional details to help maintainers reproduce the problem: 13 | 14 | * Use a clear descriptive title 15 | * Describe the exact steps to reproduce the problem. Provide as much as detail as possible. 16 | * Provide short code examples. 17 | * Describe the behavior you observed after following the steps. 18 | * Explain which behavior you expected to see instead and why. 19 | * Provide an example of execution with `DEBUG=true`. An example, `DEBUG=true node path/to/file.js` 20 | 21 | For feature requests, provide all the same detail except steps to reproduce if not applicable. 22 | 23 | ## Pull Requests 24 | Pull request are welcomed. Please make sure the following has been done: 25 | 26 | * Create a new branch by doing `git checkout -b new_feature master` 27 | * Add new test cases to test new functionality 28 | * Make sure that your tests pass with `npm test` 29 | * Commit with a clear message that explains what the commit does 30 | * Issue a pull request and make sure that TravisCI is green 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | [Describe expected behavior here] 4 | 5 | ### Actual behavior 6 | 7 | [Describe observed behavior here] 8 | 9 | ### Steps to reproduce the behavior 10 | 11 | 1. [First Step] 12 | 2. [Second Step] 13 | 3. [Other Steps...] 14 | 15 | ### Environment 16 | 17 | - OS: 18 | - Node version (`node -v`): 19 | - package version: 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Proposed changes in this pull request 2 | 3 | [Describe what this pull request does] 4 | 5 | #### Checklist 6 | * [ ] New tests have been added 7 | * [ ] `npm test` passes successfully 8 | * [ ] Documentation has been updated 9 | 10 | 11 | @amir20 to review 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib/ 3 | dist/ 4 | .idea 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | src 4 | docs 5 | __tests__ 6 | __mocks__ 7 | .* 8 | *.md 9 | examples 10 | coverage 11 | decls 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | - '11' 6 | - '10' 7 | - '9' 8 | - '8' 9 | cache: 10 | directories: 11 | - node_modules 12 | deploy: 13 | skip_cleanup: true 14 | provider: npm 15 | email: findamir@gmail.com 16 | api_key: 17 | secure: Bd1fbn3wY/zmCHWcrXU5Mo1oPDj3rprSCvU+uctDVFwb4EbCdJZv0lUU72+1u4XCQR+BUx/0x7hzFiu+qim9Y2YMsgCanUcaPvaYMW/zkLE5hDMhcV1fbFBpZQxt+UmhTuZpyGddQqZWXMsiTW696NiRpfa8hbMygcNVWQG5VzA= 18 | on: 19 | tags: true 20 | repo: amir20/phantomjs-node 21 | before_install: 22 | - npm install -g codecov 23 | after_success: 24 | - codecov 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "prettier.eslintIntegration": true, 4 | "flow.useNPMPackagedFlow": true, 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at findamir@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Amir Raminfar 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | Development on this project has been suspended due to lack of support for PhantomJs. 3 | 4 | phantom - Fast NodeJS API for PhantomJS 5 | ======== 6 | [![NPM](https://nodei.co/npm/phantom.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/phantom/) 7 | 8 | [![NPM Version][npm-image]][npm-url] 9 | [![NPM Downloads][downloads-image]][downloads-url] 10 | [![Linux Build][travis-image]][travis-url] 11 | [![Node Version][node-image]][node-url] 12 | 13 | 14 | ## Super easy to use 15 | ```js 16 | const phantom = require('phantom'); 17 | 18 | (async function() { 19 | const instance = await phantom.create(); 20 | const page = await instance.createPage(); 21 | await page.on('onResourceRequested', function(requestData) { 22 | console.info('Requesting', requestData.url); 23 | }); 24 | 25 | const status = await page.open('https://stackoverflow.com/'); 26 | const content = await page.property('content'); 27 | console.log(content); 28 | 29 | await instance.exit(); 30 | })(); 31 | 32 | ``` 33 | 34 | Using Node v7.9.0+ you can run the above example with `node file.js` 35 | 36 | See [examples](examples) folder for more ways to use this module. 37 | 38 | ## Use it with npx 39 | You can quickly test any website with phantomjs-node without needing to install the package. 40 | 41 | ``` 42 | $ npx phantom@latest https://stackoverflow.com/ 43 | ``` 44 | 45 | The above command is very useful to test if your website works on older browsers. I frequently use it to ensure [polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills) have been installed correctly. 46 | 47 | ## Deprecation warnings of PhantomJs 48 | In March 2018, the owner of PhantomJS [announced](https://phantomjs.org/) suspension of development. There hasn't been any updates since. Since phantomjs-node is only a wrapper around phantomjs, then you should use it at your own risk because the underlying dependency is no longer supported. I plan to maintain this project until usage has dropped significantly. 49 | 50 | ## Installation 51 | 52 | ### Node v6.x and later 53 | Latest version of phantom does **require Node v6.x and later**. You can install with 54 | ```bash 55 | $ npm install phantom --save 56 | ``` 57 | 58 | ### Node v5.x 59 | To use version 3.x you need to have at least Node v5+. You can install it using 60 | 61 | ```bash 62 | $ npm install phantom@3 --save 63 | ``` 64 | 65 | ### Versions _older_ than 5.x, install with 66 | 67 | ```bash 68 | $ npm install phantom@2 --save 69 | ``` 70 | 71 | ## Documents 72 | - [in website](http://amirraminfar.com/phantomjs-node/#/) 73 | - [in github](./docs/) 74 | 75 | ## Pooling 76 | 77 | Creating new phantom instances with `phantom.create()` can be slow. If 78 | you are frequently creating new instances and destroying them, as a 79 | result of HTTP requests for example, it might be worth creating a pool 80 | of instances that are re-used. 81 | 82 | See the [phantom-pool](https://github.com/blockai/phantom-pool) module 83 | for more info. 84 | 85 | ## Tests 86 | 87 | To run the test suite, first install the dependencies, then run `npm test`: 88 | 89 | ```bash 90 | $ npm install 91 | $ npm test 92 | ``` 93 | 94 | ## Contributing 95 | 96 | This package is under development. Pull requests are welcomed. Please make sure tests are added for new functionalities and that your build does pass in TravisCI. 97 | 98 | ## People 99 | 100 | The current lead maintainer is [Amir Raminfar](https://github.com/amir20) 101 | 102 | [List of all contributors](https://github.com/amir20/phantomjs-node/graphs/contributors) 103 | 104 | ## License 105 | 106 | [ISC](LICENSE.md) 107 | 108 | [npm-image]: https://img.shields.io/npm/v/phantom.svg?style=for-the-badge 109 | [npm-url]: https://npmjs.org/package/phantom 110 | [downloads-image]: https://img.shields.io/npm/dm/phantom.svg?style=for-the-badge 111 | [downloads-url]: https://npmjs.org/package/phantom 112 | [travis-image]: https://img.shields.io/travis/amir20/phantomjs-node.svg?style=for-the-badge 113 | [travis-url]: https://travis-ci.org/amir20/phantomjs-node 114 | [dependencies-image]: https://dependencyci.com/github/amir20/phantomjs-node/badge?style=for-the-badge 115 | [dependencies-url]: https://dependencyci.com/github/amir20/phantomjs-node 116 | [node-image]: https://img.shields.io/node/v/phantom.svg?style=for-the-badge 117 | [node-url]: https://nodejs.org/en/download/ 118 | [codecov-image]: https://codecov.io/gh/amir20/phantomjs-node/branch/master/graph/badge.svg?style=for-the-badge 119 | [codecov-url]: https://codecov.io/gh/amir20/phantomjs-node 120 | -------------------------------------------------------------------------------- /bin/phantom.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const phantom = require('phantom'); // eslint-disable-line 4 | 5 | const [, , url] = process.argv; 6 | 7 | (async function main() { 8 | const instance = await phantom.create(); 9 | const page = await instance.createPage(); 10 | await page.on('onResourceRequested', (requestData) => { 11 | console.info('Requesting', requestData.url); // eslint-disable-line 12 | }); 13 | 14 | await page.open(url); 15 | const content = await page.property('content'); 16 | console.log(content); // eslint-disable-line 17 | 18 | await instance.exit(); 19 | }()); 20 | -------------------------------------------------------------------------------- /decls/config.js: -------------------------------------------------------------------------------- 1 | declare class Config { 2 | logger: Logger; 3 | phantomPath: string; 4 | shimPath: string; 5 | logLevel: string; 6 | } 7 | -------------------------------------------------------------------------------- /decls/logger.js: -------------------------------------------------------------------------------- 1 | declare type Logger = { 2 | info(s: string, ...params: any[]): void; 3 | debug(s: string, ...params: any[]): void; 4 | error(s: string, ...params: any[]): void; 5 | warn(s: string, ...params: any[]): void; 6 | } 7 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amir20/phantomjs-node/dec84c74ce16674e0d6f74307f505a539d163e6e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## PHANTOM 2 | 3 | > Fast NodeJS API for PhantomJS. 4 | 5 | - **WEBSITE:** http://amirraminfar.com/phantomjs-node/ 6 | - **GITHUB:** https://github.com/amir20/phantomjs-node 7 | 8 | ## Super easy to use 9 | ```js 10 | const phantom = require('phantom'); 11 | 12 | (async function() { 13 | const instance = await phantom.create(); 14 | const page = await instance.createPage(); 15 | await page.on('onResourceRequested', function(requestData) { 16 | console.info('Requesting', requestData.url); 17 | }); 18 | 19 | const status = await page.open('https://stackoverflow.com/'); 20 | const content = await page.property('content'); 21 | console.log(content); 22 | 23 | await instance.exit(); 24 | })(); 25 | 26 | ``` 27 | 28 | Using Node v7.9.0+ you can run the above example with `node file.js` 29 | 30 | See [examples](https://github.com/amir20/phantomjs-node/tree/master/examples) folder for more ways to use this module. 31 | -------------------------------------------------------------------------------- /docs/_media/example.html: -------------------------------------------------------------------------------- 1 |

To infinity and Beyond!

-------------------------------------------------------------------------------- /docs/_media/example.md: -------------------------------------------------------------------------------- 1 | > This is from the `example.md` 2 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - Translations 2 | - [:uk: English](./) 3 | - [:cn: 中文](./zh-cn/) 4 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Installation](install.md) 2 | * [How does it work?](work.md) 3 | * [Migrating](migrating.md) 4 | 5 | * [Phantom object API](phantom_obj.md) 6 | * [Page object API](page_obj.md) 7 | * [Advanced](advanced.md) 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | Methods below are for advanced users. Most people won't need these methods. 3 | 4 | ## page.defineMethod 5 | 6 | A method can be defined using the `#defineMethod(name, definition)` method. 7 | 8 | ```js 9 | page.defineMethod('getZoomFactor', function() { 10 | return this.zoomFactor; 11 | }); 12 | ``` 13 | 14 | ## page.invokeAsyncMethod 15 | 16 | An asynchronous method can be invoked using the `#invokeAsyncMethod(method, arg1, arg2, arg3...)` method. 17 | 18 | ```js 19 | page.invokeAsyncMethod('open', 'http://phantomjs.org/').then(function(status) { 20 | console.log(status); 21 | }); 22 | ``` 23 | 24 | ## page.invokeMethod 25 | 26 | A method can be invoked using the `#invokeMethod(method, arg1, arg2, arg3...)` method. 27 | 28 | ```js 29 | page.invokeMethod('evaluate', function() { 30 | return document.title; 31 | }).then(function(title) { 32 | console.log(title); 33 | }); 34 | ``` 35 | 36 | ```js 37 | page.invokeMethod('evaluate', function(selector) { 38 | return document.querySelector(selector) !== null; 39 | }, '#element').then(function(exists) { 40 | console.log(exists); 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PHANTOM 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 |
Loading ...
22 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Node v6.x and later 4 | Latest version of phantom does **require Node v6.x and later**. You can install with 5 | ```bash 6 | $ npm install phantom --save 7 | ``` 8 | 9 | ## Node v5.x 10 | To use version 3.x you need to have at least Node v5+. You can install it using 11 | 12 | ```bash 13 | $ npm install phantom@3 --save 14 | ``` 15 | 16 | ## Versions _older_ than 5.x, install with 17 | 18 | ```bash 19 | $ npm install phantom@2 --save 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/migrating.md: -------------------------------------------------------------------------------- 1 | ## Migrating from 2.x 2 | 3 | Going forward, version phantom@3 will only support Node v5 and above. This adds the extra benefit of less code and faster performance. 4 | 5 | ## Migrating from 1.0.x 6 | 7 | Version 2.0.x is not backward compatible with previous versions. Most notability, method calls do not take a callback function anymore. Since `node` supports `Promise`, each of the methods return a promise. Instead of writing `page.open(url, function(){})` you would have to write `page.open(url).then(function(){})`. 8 | 9 | The API is much more consistent now. All properties can be read with `page.property(key)` and settings can be read with `page.setting(key)`. See below for more example. 10 | -------------------------------------------------------------------------------- /docs/page_obj.md: -------------------------------------------------------------------------------- 1 | # page object API 2 | 3 | The `page` object that is returned with `#createPage` is a proxy that sends all methods to `phantom`. Most method calls should be identical to PhantomJS API. You must remember that each method returns a `Promise`. 4 | 5 | ## setting 6 | 7 | `page.settings` can be accessed via `page.setting(key)` or set via `page.setting(key, value)`. Here is an example to read `javascriptEnabled` property. 8 | 9 | ```js 10 | page.setting('javascriptEnabled').then(function(value){ 11 | expect(value).toEqual(true); 12 | }); 13 | ``` 14 | 15 | ## property 16 | 17 | 18 | Page properties can be read using the `#property(key)` method. 19 | 20 | ```js 21 | page.property('plainText').then(function(content) { 22 | console.log(content); 23 | }); 24 | ``` 25 | 26 | Page properties can be set using the `#property(key, value)` method. 27 | 28 | ```js 29 | page.property('viewportSize', {width: 800, height: 600}).then(function() { 30 | }); 31 | ``` 32 | When setting values, using `then()` is optional. But beware that the next method to phantom will block until it is ready to accept a new message. 33 | 34 | ~~You can set events using `#property()` because they are property members of `page`.~~ 35 | 36 | ```js 37 | page.property('onResourceRequested', function(requestData, networkRequest) { 38 | console.log(requestData.url); 39 | }); 40 | ``` 41 | ~~It is important to understand that the function above executes in the PhantomJS process. PhantomJS does not share any memory or variables with node. So using closures in javascript to share any variables outside of the function is not possible. Variables can be passed to `#property` instead. So for example, let's say you wanted to pass `process.env.DEBUG` to `onResourceRequested` method above. You could do this by:~~ 42 | 43 | **Using `page#property` to set events will be deprecated in next release. Please use `page#on()` instead.** 44 | 45 | Even if it is possible to set the events using this way, we recommend you use `#on()` for events (see below). 46 | 47 | 48 | You can return data to NodeJS by using `#createOutObject()`. This is a special object that let's you write data in PhantomJS and read it in NodeJS. Using the example above, data can be read by doing: 49 | 50 | ```js 51 | var outObj = phInstance.createOutObject(); 52 | outObj.urls = []; 53 | page.property('onResourceRequested', function(requestData, networkRequest, out) { 54 | out.urls.push(requestData.url); 55 | }, outObj); 56 | 57 | // after call to page.open() 58 | outObj.property('urls').then(function(urls){ 59 | console.log(urls); 60 | }); 61 | 62 | ``` 63 | 64 | ## on 65 | 66 | By using `on(event, [runOnPhantom=false],listener, args*)`, you can listen to the events the page emits. 67 | 68 | ```js 69 | var urls = []; 70 | 71 | page.on('onResourceRequested', function (requestData, networkRequest) { 72 | urls.push(requestData.url); // this would push the url into the urls array above 73 | networkRequest.abort(); // This will fail, because the params are a serialized version of what was provided 74 | }); 75 | 76 | page.open('http://google.com'); 77 | ``` 78 | As you see, using on you have access to the closure variables and all the node goodness using this function ans in contrast of setting and event with property, you can set as many events as you want. 79 | 80 | If you want to register a listener to run in phantomjs runtime (and thus, be able to cancel the request lets say), you can make it by passing the optional param `runOnPhantom` as `true`; 81 | 82 | ```js 83 | var urls = []; 84 | 85 | page.on('onResourceRequested', true, function (requestData, networkRequest) { 86 | urls.push(requestData.url); // now this wont work, because this function would execute in phantom runtime and thus wont have access to the closure. 87 | networkRequest.abort(); // This would work, because you are accessing to the non serialized networkRequest. 88 | }); 89 | 90 | page.open('http://google.com'); 91 | ``` 92 | The same as in property, you can pass additional params to the function in the same way, and even use the object created by `#createOutObject()`. 93 | 94 | You cannot use `#property()` and `#on()` at the same time, because it would conflict. Property just sets the function in phantomjs, while `#on()` manages the event in a different way. 95 | 96 | ## off 97 | 98 | `#off(event)` is usefull to remove all the event listeners set by `#on()` for ans specific event. 99 | 100 | ## evaluate 101 | 102 | Using `#evaluate()` is similar to passing a function above. For example, to return HTML of an element you can do: 103 | 104 | ```js 105 | page.evaluate(function() { 106 | return document.getElementById('foo').innerHTML; 107 | }).then(function(html){ 108 | console.log(html); 109 | }); 110 | ``` 111 | 112 | ## evaluateAsync 113 | 114 | Same as `#evaluate()`, but function will be executed asynchronously and there is no return value. You can specify delay of execution. 115 | 116 | ```js 117 | page.evaluateAsync(function(apiUrl) { 118 | $.ajax({url: apiUrl, success: function() {}}); 119 | }, 0, "http://mytestapi.com") 120 | ``` 121 | 122 | ## evaluateJavaScript 123 | 124 | Evaluate a function contained in a string. It is similar to `#evaluate()`, but the function can't take any arguments. This example does the same thing as the example of `#evaluate()`: 125 | 126 | ```js 127 | page.evaluateJavaScript('function() { return document.getElementById(\'foo\').innerHTML; }').then(function(html){ 128 | console.log(html); 129 | }); 130 | ``` 131 | 132 | ## switchToFrame 133 | 134 | Switch to the frame specified by a frame name or a frame position: 135 | 136 | ```js 137 | page.switchToFrame(framePositionOrName).then(function() { 138 | // now the context of `page` will be the iframe if frame name or position exists 139 | }); 140 | ``` 141 | 142 | ## switchToMainFrame 143 | 144 | Switch to the main frame of the page: 145 | 146 | ```js 147 | page.switchToMainFrame().then(function() { 148 | // now the context of `page` will the main frame 149 | }); 150 | ``` 151 | 152 | ## uploadFile 153 | 154 | A file can be inserted into file input fields using the `#uploadFile(selector, file)` method. 155 | 156 | ```js 157 | page.uploadFile('#selector', '/path/to/file').then(function() { 158 | 159 | }); 160 | ``` 161 | -------------------------------------------------------------------------------- /docs/phantom_obj.md: -------------------------------------------------------------------------------- 1 | # phantom object API 2 | 3 | ## create 4 | 5 | To create a new instance of `phantom` use `phantom.create()` which returns a `Promise` which should resolve with a `phantom` object. 6 | If you want add parameters to the phantomjs process you can do so by doing: 7 | 8 | ```js 9 | var phantom = require('phantom'); 10 | phantom.create(['--ignore-ssl-errors=yes', '--load-images=no']).then(...) 11 | ``` 12 | You can also explicitly set : 13 | 14 | - The phantomjs path to use 15 | - The path to the shim/index.js to use 16 | - A logger object 17 | - A log level if no logger was specified 18 | 19 | by passing them in config object: 20 | ```js 21 | var phantom = require('phantom'); 22 | phantom.create([], { 23 | phantomPath: '/path/to/phantomjs', 24 | shimPath: '/path/to/shim/index.js', 25 | logger: yourCustomLogger, 26 | logLevel: 'debug', 27 | }).then(...) 28 | ``` 29 | 30 | The `logger` parameter should be a `logger` object containing your logging functions. The `logLevel` parameter should be log level like `"warn"` or `"debug"` (It uses the same log levels as `npm`), and will be ignored if `logger` is set. Have a look at the `logger` property below for more information about these two parameters. 31 | 32 | ## createPage 33 | 34 | To create a new `page`, you have to call `createPage()`: 35 | 36 | ```js 37 | var sitepage = null; 38 | var phInstance = null; 39 | phantom.create() 40 | .then(instance => { 41 | phInstance = instance; 42 | return instance.createPage(); 43 | }) 44 | .then(page => { 45 | // use page 46 | }) 47 | .catch(error => { 48 | console.log(error); 49 | phInstance.exit(); 50 | }); 51 | ``` 52 | 53 | ## exit 54 | 55 | Sends an exit call to phantomjs process. 56 | 57 | Make sure to call it on the phantom instance to kill the phantomjs process. Otherwise, the process will never exit. 58 | 59 | ## kill 60 | 61 | Kills the underlying phantomjs process (by sending `SIGKILL` to it). 62 | 63 | It may be a good idea to register handlers to `SIGTERM` and `SIGINT` signals with `#kill()`. 64 | 65 | However, be aware that phantomjs process will get detached (and thus won't exit) if node process that spawned it receives `SIGKILL`! 66 | 67 | ## logger 68 | 69 | The property containing the [winston](https://www.npmjs.com/package/winston) `logger` used by a `phantom` instance. You may change parameters like verbosity or redirect messages to a file with it. 70 | 71 | You can also use your own logger by providing it to the `create` method. The `logger` object can contain four functions : `debug`, `info`, `warn` and `error`. If one of them is empty, its output will be discarded. 72 | 73 | Here are two ways of handling it : 74 | ```js 75 | /* Set the log level to 'error' at creation, and use the default logger */ 76 | phantom.create([], { logLevel: 'error' }).then(function(ph) { 77 | // use ph 78 | }); 79 | 80 | /* Set a custom logger object directly in the create call. Note that `info` is not provided here and so its output will be discarded */ 81 | var log = console.log; 82 | var nolog = function() {}; 83 | phantom.create([], { logger: { warn: log, debug: nolog, error: log } }).then(function(ph) { 84 | // use ph 85 | }); 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/update_log.md: -------------------------------------------------------------------------------- 1 | # update log 2 | -------------------------------------------------------------------------------- /docs/work.md: -------------------------------------------------------------------------------- 1 | # How does it work? 2 | 3 | [v1.0.x](//github.com/amir20/phantomjs-node/tree/v1) used to leverage `dnode` to communicate between nodejs and phantomjs. This approach raised a lot of security restrictions and did not work well when using `cluster` or `pm2`. 4 | 5 | v2.0.x has been completely rewritten to use `sysin` and `sysout` pipes to communicate with the phantomjs process. It works out of the box with `cluster` and `pm2`. If you want to see the messages that are sent try adding `DEBUG=true` to your execution, ie. `DEBUG=true node path/to/test.js`. The new code is much cleaner and simpler. PhantomJS is started with a shim which proxies all messages to the `page` or `phantom` object. 6 | -------------------------------------------------------------------------------- /docs/zh-cn/README.md: -------------------------------------------------------------------------------- 1 | # PHANTOM 2 | 3 | > `PhantomJS` 的 node 版本。 4 | 5 | - **官网:** http://amirraminfar.com/phantomjs-node/ 6 | - **GITHUB:** https://github.com/amir20/phantomjs-node 7 | 8 | ## 是什么 9 | 10 | 可以方便的在 node 程序中使用 `PhantomJS` 相关的 api 。 11 | 12 | ## 例子 13 | 14 | ``` js 15 | const phantom = require('phantom'); 16 | 17 | (async function() { 18 | const instance = await phantom.create(); 19 | const page = await instance.createPage(); 20 | await page.on('onResourceRequested', function(requestData) { 21 | console.info('Requesting', requestData.url); 22 | }); 23 | 24 | const status = await page.open('https://stackoverflow.com/'); 25 | const content = await page.property('content'); 26 | console.log(content); 27 | 28 | await instance.exit(); 29 | })(); 30 | ``` 31 | 更多例子: 32 | 33 | [examples](https://github.com/amir20/phantomjs-node/tree/master/examples) 。 34 | -------------------------------------------------------------------------------- /docs/zh-cn/_navbar.md: -------------------------------------------------------------------------------- 1 | - Translations 2 | - [:uk: English](../) 3 | - [:cn: 中文](../zh-cn/) 4 | -------------------------------------------------------------------------------- /docs/zh-cn/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [上手](zh-cn/install.md) 2 | * [工作原理](zh-cn/work.md) 3 | * [版本迁移](zh-cn/migrating.md) 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/zh-cn/advanced.md: -------------------------------------------------------------------------------- 1 | # 高级用法 2 | -------------------------------------------------------------------------------- /docs/zh-cn/install.md: -------------------------------------------------------------------------------- 1 | # 上手 2 | **node v6.x +** 3 | 6.x 及更高版本: 4 | 5 | `npm install phantom --save` 6 | 7 | **Node v5.x +** 8 | `npm install phantom@3 --save` 9 | 10 | **Node v5.x -** 11 | `npm install phantom@2 --save` 12 | -------------------------------------------------------------------------------- /docs/zh-cn/migrating.md: -------------------------------------------------------------------------------- 1 | # 版本迁移 2 | ## 2.x 3 | 为了更好的发展空间, `phantom@3` 只支持 `node v5` 以上版本。 4 | 代码更少,性能更快。 5 | 6 | ## 1.0.x 7 | 版本 2.0.x 不再使用回调函数。而使用 `Promise` 。 8 | 例如 `page.open(url, function(){})` => `page.open(url).then(function(){})` 。 9 | 10 | API 更加一致, 所有属性都只可以读取。 11 | 如 `page.property(key)` => `page.setting(key)` 。 12 | 13 | 更多 API 请参见后文。 14 | -------------------------------------------------------------------------------- /docs/zh-cn/page_obj.md: -------------------------------------------------------------------------------- 1 | # page 对象的 API 2 | -------------------------------------------------------------------------------- /docs/zh-cn/phantom_obj.md: -------------------------------------------------------------------------------- 1 | # phantom 对象的 API 2 | -------------------------------------------------------------------------------- /docs/zh-cn/update_log.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | -------------------------------------------------------------------------------- /docs/zh-cn/work.md: -------------------------------------------------------------------------------- 1 | # 工作原理 2 | [v1.0.x](//github.com/amir20/phantomjs-node/tree/v1) 利用 dnode 在 nodejs 和 phantomjs 之间进行通信。 3 | 这种方法有很多安全限制,并且在使用 raised 或 pm2 时效果并不理想。 4 | 5 | v2.2.x 使用 SysIn 和 SysOUT 管道与 phantomjs 进程通信。并且代码更简洁。 6 | 7 | 它可以用于 raised 和 pm2 工作。 8 | 如果你想看到调试信息,可以在运行时使用 `DEBUG=true` 。 9 | 如 `DEBUG=true node test.js` 。 10 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["syntax-async-functions", "transform-regenerator"] 4 | } -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | MAINTAINER Amir Raminfar 3 | 4 | RUN apt-get update && apt-get install -y curl 5 | RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 6 | RUN apt-get install -y nodejs build-essential g++ flex bison gperf ruby perl \ 7 | libsqlite3-dev libfontconfig1-dev libicu-dev libfreetype6 libssl-dev \ 8 | libpng-dev libjpeg-dev python libx11-dev libxext-dev 9 | 10 | RUN npm install phantom 11 | 12 | ADD async.js /async.js 13 | 14 | ENTRYPOINT ["node", "/async.js"] 15 | CMD ["http://stackoverflow.com"] 16 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | const phantom = require('phantom'); 2 | 3 | (async function () { 4 | const instance = await phantom.create(); 5 | const page = await instance.createPage(); 6 | await page.on('onResourceRequested', function (requestData) { 7 | console.info('Requesting', requestData.url); 8 | }); 9 | 10 | const status = await page.open(process.argv[2]); 11 | const content = await page.property('content'); 12 | console.log(content); 13 | 14 | await instance.exit(); 15 | })(); 16 | 17 | // node async.js http://stackoverflow.com 18 | -------------------------------------------------------------------------------- /examples/cluster.js: -------------------------------------------------------------------------------- 1 | var cluster = require("cluster"); 2 | 3 | if (cluster.isMaster) { 4 | var cpuCount = require('os').cpus().length; 5 | 6 | for (var i = 0; i < cpuCount; i++) { 7 | console.log('Forking process #' + (i + 1)); 8 | cluster.fork(); 9 | } 10 | 11 | cluster.on('exit', function (worker) { 12 | console.log('Worker ' + worker.id + ' died. Forking...'); 13 | cluster.fork(); 14 | }); 15 | 16 | } else { 17 | var phantom = require("phantom"), 18 | express = require("express"), 19 | serve = express(); 20 | 21 | serve.get('/foo', function (req, res) { 22 | phantom.create().then(function (ph) { 23 | ph.createPage().then(function (page) { 24 | page.setting('userAgent', 'foo app'); 25 | page.open('http://localhost:8080/test.html').then(function (status) { 26 | res.json({ 27 | pageStatus: status 28 | }); 29 | page.close(); 30 | ph.exit(); 31 | }); 32 | }); 33 | }); 34 | }).listen(3000); 35 | } 36 | 37 | // npm install express 38 | // node cluster.js 39 | -------------------------------------------------------------------------------- /examples/co.js: -------------------------------------------------------------------------------- 1 | var phantom = require('phantom'); 2 | var co = require('co'); 3 | 4 | co(function*() { 5 | var instance = yield phantom.create(); 6 | try { 7 | var page = yield instance.createPage(); 8 | var status = yield page.open('https://stackoverflow.com/'); 9 | console.log(status); 10 | var content = yield page.property('content'); 11 | console.log(content); 12 | } catch (e) { 13 | console.log('Error found: ' + e.message); 14 | } finally { 15 | instance.exit(); 16 | } 17 | }).catch(console.error); 18 | -------------------------------------------------------------------------------- /examples/out_obj.js: -------------------------------------------------------------------------------- 1 | import phantom from 'phantom'; 2 | 3 | let _ph, _page, _outObj; 4 | 5 | phantom 6 | .create() 7 | .then(ph => { 8 | _ph = ph; 9 | return _ph.createPage(); 10 | }) 11 | .then(page => { 12 | _page = page; 13 | _outObj = _ph.createOutObject(); 14 | 15 | _outObj.urls = []; 16 | page.property( 17 | 'onResourceRequested', 18 | function(requestData, networkRequest, out) { 19 | out.urls.push(requestData.url); 20 | }, 21 | _outObj, 22 | ); 23 | 24 | return _page.open('https://stackoverflow.com/'); 25 | }) 26 | .then(status => { 27 | return _outObj.property('urls'); 28 | }) 29 | .then(urls => { 30 | console.log(urls); 31 | _page.close(); 32 | _ph.exit(); 33 | }) 34 | .catch(console.error); 35 | 36 | // babel-node out_obj.js 37 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomjs-node-examples", 3 | "license": "ISC", 4 | "dependencies": { 5 | "co": "^4.6.0", 6 | "phantom": "file:../" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/render.js: -------------------------------------------------------------------------------- 1 | const phantom = require('phantom'); 2 | 3 | (async function() { 4 | const instance = await phantom.create(); 5 | const page = await instance.createPage(); 6 | 7 | await page.property('viewportSize', { width: 1024, height: 600 }); 8 | const status = await page.open('https://stackoverflow.com/'); 9 | console.log(`Page opened with status [${status}].`); 10 | 11 | await page.render('stackoverflow.pdf'); 12 | console.log(`File created at [./stackoverflow.pdf]`); 13 | 14 | await instance.exit(); 15 | })(); 16 | 17 | // node --harmony-async-await render.js 18 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var phantom = require('phantom'); 2 | var _ph, _page, _outObj; 3 | 4 | phantom 5 | .create() 6 | .then(ph => { 7 | _ph = ph; 8 | return _ph.createPage(); 9 | }) 10 | .then(page => { 11 | _page = page; 12 | return _page.open('https://stackoverflow.com/'); 13 | }) 14 | .then(status => { 15 | console.log(status); 16 | return _page.property('content'); 17 | }) 18 | .then(content => { 19 | console.log(content); 20 | _page.close(); 21 | _ph.exit(); 22 | }) 23 | .catch(e => console.log(e)); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Amir Raminfar ", 3 | "name": "phantom", 4 | "description": "PhantomJS integration module for NodeJS", 5 | "homepage": "https://github.com/amir20/phantomjs-node", 6 | "version": "6.3.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/amir20/phantomjs-node.git" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Amir Raminfar", 14 | "email": "findamir@gmail.com", 15 | "web": "http://amirraminfar.com/" 16 | } 17 | ], 18 | "keywords": [ 19 | "phantom", 20 | "phantomjs", 21 | "driver" 22 | ], 23 | "main": "dist/index.js", 24 | "engines": { 25 | "node": ">=8" 26 | }, 27 | "dependencies": { 28 | "phantomjs-prebuilt": "^2.1.16", 29 | "split": "^1.0.1", 30 | "winston": "^3.2.1" 31 | }, 32 | "bin": { 33 | "phantom": "./bin/phantom.js" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.26.0", 37 | "babel-core": "^6.26.3", 38 | "babel-eslint": "^10.0.1", 39 | "babel-polyfill": "^6.26.0", 40 | "babel-preset-env": "^1.7.0", 41 | "babel-preset-es2015": "^6.18.0", 42 | "babel-preset-flow": "^6.23.0", 43 | "eslint": "^5.15.3", 44 | "eslint-config-airbnb-base": "^13.1.0", 45 | "eslint-plugin-flowtype": "^3.4.2", 46 | "eslint-plugin-import": "^2.16.0", 47 | "flow-bin": "^0.102.0", 48 | "flow-copy-source": "^2.0.7", 49 | "jest": "^24.8.0", 50 | "jest-cli": "^23.6.0", 51 | "npm-watch": "^0.6.0", 52 | "prettier-eslint": "^9.0.0", 53 | "rimraf": "^2.6.3" 54 | }, 55 | "license": "ISC", 56 | "jest": { 57 | "roots": [ 58 | "/dist" 59 | ], 60 | "testMatch": [ 61 | "**/?(*.)+(spec|test).js" 62 | ], 63 | "collectCoverage": true, 64 | "coveragePathIgnorePatterns": [ 65 | "node_modules/", 66 | "jest-modules/" 67 | ], 68 | "testEnvironment": "node" 69 | }, 70 | "watch": { 71 | "build": "src" 72 | }, 73 | "scripts": { 74 | "lint": "eslint . && flow check", 75 | "pretest": "npm run lint && npm run build", 76 | "test": "jest", 77 | "clean": "rimraf dist", 78 | "flowbuild": "flow-copy-source src dist", 79 | "prebuild": "npm run clean && npm run flowbuild", 80 | "build": "babel src -d dist", 81 | "prepublishOnly": "npm run build", 82 | "watch": "npm-watch" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/__mocks__/child_process.js: -------------------------------------------------------------------------------- 1 | let mockedSpawn; 2 | function setMockedSpawn(mock) { 3 | mockedSpawn = mock; 4 | } 5 | 6 | const spawn = (...args) => mockedSpawn(...args); 7 | 8 | export { spawn, setMockedSpawn }; 9 | -------------------------------------------------------------------------------- /src/__tests__/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "current" 6 | } 7 | }], 8 | "flow" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/command.test.js: -------------------------------------------------------------------------------- 1 | import Command from '../command'; 2 | 3 | describe('Command', () => { 4 | it('id to be randomly generated', () => { 5 | expect(new Command('test').id).toEqual(new Command().id - 1); 6 | }); 7 | 8 | it('.target to be set correctly', () => { 9 | expect(new Command('abc').target).toEqual('abc'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import phantom from '../index'; 2 | import Phantom from '../phantom'; 3 | 4 | describe('index.js', () => { 5 | it('phantom#create().then() returns a new Phantom instance', () => phantom.create().then((ph) => { 6 | expect(ph).toBeInstanceOf(Phantom); 7 | ph.exit(); 8 | })); 9 | 10 | it('phantom#create() returns a new Promise instance', () => { 11 | const promise = phantom.create(); 12 | expect(promise).toBeInstanceOf(Promise); 13 | return promise.then(ph => ph.exit()); 14 | }); 15 | 16 | it('phantom#create([], {}).then() with a custom shim path returns a new Phantom instance', () => phantom.create([], { shimPath: `${__dirname}/shim/index.js` }).then((ph) => { 17 | expect(ph).toBeInstanceOf(Phantom); 18 | ph.exit(); 19 | })); 20 | 21 | it('#create([], {}) errors with undefined phantomjs-prebuilt to throw exception', async () => { 22 | await expect(phantom.create([], { phantomPath: null })).rejects 23 | .toEqual(new Error('PhantomJS binary was not found. ' 24 | + 'This generally means something went wrong when installing phantomjs-prebuilt. Exiting.')); 25 | }); 26 | 27 | it('#create([], {}) errors with non-string passed in as shimPath', async () => { 28 | await expect(phantom.create([], { shimPath: 12 })).rejects 29 | .toEqual(new Error('Path to shim file was not found. ' 30 | + 'Are you sure you entered the path correctly? Exiting.')); 31 | }); 32 | 33 | it('#create([], {}) errors with string for logger', async () => { 34 | await expect(phantom.create([], { logger: 'log' })).rejects 35 | .toEqual(new Error('logger must be a valid object.')); 36 | }); 37 | 38 | it('#create([], {}) errors with string for logger', async () => { 39 | await expect(phantom.create('str')).rejects 40 | .toEqual(new Error('Unexpected type of parameters. Expecting args to be array.')); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/inject_example.js: -------------------------------------------------------------------------------- 1 | window.foo = 1; 2 | -------------------------------------------------------------------------------- /src/__tests__/out_object.test.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import Phantom from '../phantom'; 3 | import OutObject from '../out_object'; 4 | 5 | describe('Command', () => { 6 | let phantom; 7 | 8 | beforeEach(() => { 9 | phantom = new Phantom(); 10 | }); 11 | afterEach(() => phantom.exit()); 12 | 13 | it('target to be set', () => { 14 | expect(phantom.createOutObject().target).toBeDefined(); 15 | }); 16 | 17 | it('#createOutObject() is a valid OutObject', () => { 18 | const outObj = phantom.createOutObject(); 19 | expect(outObj).toBeInstanceOf(OutObject); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/page.evaluate.test.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import 'babel-polyfill'; 3 | import Phantom from '../phantom'; 4 | 5 | describe('Page', () => { 6 | let server; 7 | let phantom; 8 | let port; 9 | beforeAll((done) => { 10 | server = http.createServer((request, response) => { 11 | response.end('Page TitleTest'); 12 | }); 13 | 14 | server.listen(0, () => { 15 | port = server.address().port; // eslint-disable-line 16 | done(); 17 | }); 18 | }); 19 | 20 | afterAll(() => server.close()); 21 | beforeEach(() => { 22 | phantom = new Phantom(); 23 | }); 24 | afterEach(() => phantom.exit()); 25 | 26 | fit('#evaluate(function(){return document.title}) executes correctly', async () => { 27 | const page = await phantom.createPage(); 28 | await page.open(`http://localhost:${port}/test.html`); 29 | const response = await page.evaluate(function() { return document.title }); // eslint-disable-line 30 | expect(response).toEqual('Page Title'); 31 | }); 32 | 33 | it('#evaluate(function(){...}) executes correctly', async () => { 34 | const page = await phantom.createPage(); 35 | const response = await page.evaluate(function() { return 'test' }); // eslint-disable-line 36 | expect(response).toEqual('test'); 37 | }); 38 | 39 | it('#evaluate(function(arg){...}, argument) executes correctly with a non-null argument', async () => { 40 | const page = await phantom.createPage(); 41 | const response = await page.evaluate(function(arg) { return 'Value: ' + arg}, 'test'); // eslint-disable-line 42 | expect(response).toEqual('Value: test'); 43 | }); 44 | 45 | it('#evaluate(function(arg){...}, argument) executes correctly with a null argument', async () => { 46 | const page = await phantom.createPage(); 47 | const response = await page.evaluate(function(arg) { return 'Value is null:' + arg === null}, 'test'); // eslint-disable-line 48 | expect(response).toEqual('Value is null: true'); 49 | }); 50 | 51 | it('#evaluateAsync(function(){...}) executes correctly', async () => { 52 | const page = await phantom.createPage(); 53 | await page.on('onCallback', (response) => { 54 | expect(response).toEqual('test'); 55 | }); 56 | await page.evaluateAsync(() => { 57 | window.callPhantom('test'); 58 | }); 59 | }); 60 | 61 | it('#evaluateAsync(function(){...}) executes correctly with a delay and a non-null argument', async () => { 62 | const page = await phantom.createPage(); 63 | await page.on('onCallback', (response) => { 64 | expect(response).toEqual('testarg'); 65 | }); 66 | await page.evaluateAsync( 67 | (arg) => { 68 | window.callPhantom(`test${arg}`); 69 | }, 70 | 0, 71 | 'arg', 72 | ); 73 | }); 74 | 75 | it("#evaluateJavaScript('function(){return document.title}') executes correctly", async () => { 76 | const page = await phantom.createPage(); 77 | await page.open(`http://localhost:${port}/test.html`); 78 | const response = await page.evaluateJavaScript('function () { return document.title }'); 79 | expect(response).toEqual('Page Title'); 80 | }); 81 | 82 | it("#evaluateJavaScript('function(){...}') executes correctly", async () => { 83 | const page = await phantom.createPage(); 84 | const response = await page.evaluateJavaScript("function () { return 'test' }"); 85 | expect(response).toEqual('test'); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/__tests__/page.event.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | 3 | import 'babel-polyfill'; 4 | import http from 'http'; 5 | import Phantom from '../phantom'; 6 | 7 | describe('Page', () => { 8 | let server; 9 | let phantom; 10 | let port; 11 | beforeAll((done) => { 12 | server = http.createServer((request, response) => { 13 | response.end(`hi, ${request.url}`); 14 | }); 15 | server.listen(0, () => { 16 | port = server.address().port; // eslint-disable-line 17 | done(); 18 | }); 19 | }); 20 | 21 | afterAll(() => server.close()); 22 | beforeEach(() => { 23 | phantom = new Phantom(); 24 | }); 25 | afterEach(() => phantom.exit()); 26 | 27 | it('#on() can register an event in the page and run the code locally', async () => { 28 | const page = await phantom.createPage(); 29 | let ranHere = false; 30 | 31 | await page.on('onResourceReceived', () => { 32 | ranHere = true; 33 | }); 34 | 35 | await page.open(`http://localhost:${port}/test`); 36 | 37 | expect(ranHere).toBe(true); 38 | }); 39 | 40 | it('#on() event registered does not run if not triggered', async () => { 41 | const page = await phantom.createPage(); 42 | let ranHere = false; 43 | 44 | await page.on('onResourceReceived', () => { 45 | ranHere = true; 46 | }); 47 | 48 | expect(ranHere).toBe(false); 49 | }); 50 | 51 | it('#on() can register more than one event of the same type', async () => { 52 | const page = await phantom.createPage(); 53 | let ranHere = false; 54 | 55 | await page.on('onResourceReceived', () => { 56 | ranHere = true; 57 | }); 58 | 59 | let runnedHereToo = false; 60 | 61 | await page.on('onResourceReceived', () => { 62 | runnedHereToo = true; 63 | }); 64 | 65 | await page.open(`http://localhost:${port}/test`); 66 | 67 | expect(ranHere).toBe(true); 68 | expect(runnedHereToo).toBe(true); 69 | }); 70 | 71 | it('#on() can pass parameters', async () => { 72 | const page = await phantom.createPage(); 73 | let parameterProvided = false; 74 | 75 | await page.on( 76 | 'onResourceReceived', 77 | (status, param) => { 78 | parameterProvided = param; 79 | }, 80 | 'param', 81 | ); 82 | 83 | await page.open(`http://localhost:${port}/test`); 84 | 85 | expect(parameterProvided).toBe('param'); 86 | }); 87 | 88 | it('#on() can register an event in the page which code runs in phantom runtime', async () => { 89 | const page = await phantom.createPage(); 90 | let ranHere = false; 91 | 92 | // eslint-disable-next-line 93 | await page.on('onLoadFinished', true, function() { 94 | ranHere = true; 95 | ranInPhantomRuntime = true; 96 | }); 97 | 98 | await page.open(`http://localhost:${port}/test`); 99 | 100 | let ranInPhantomRuntime = await phantom.windowProperty('ranInPhantomRuntime'); 101 | 102 | expect(ranHere).toBe(false); 103 | expect(ranInPhantomRuntime).toBe(true); 104 | }); 105 | 106 | it('#on() can pass parameters to functions to be executed in phantom runtime', async () => { 107 | const page = await phantom.createPage(); 108 | 109 | await page.on( 110 | 'onResourceReceived', 111 | true, 112 | function(status, param) { // eslint-disable-line 113 | parameterProvided = param; 114 | }, 115 | 'param', 116 | ); 117 | 118 | await page.open(`http://localhost:${port}/test`); 119 | 120 | let parameterProvided = await phantom.windowProperty('parameterProvided'); 121 | 122 | expect(parameterProvided).toBe('param'); 123 | }); 124 | 125 | it('#on() event supposed to run in phantom runtime wont run if not triggered', async () => { 126 | const page = await phantom.createPage(); 127 | 128 | // eslint-disable-next-line 129 | await page.on('onResourceReceived', true, function() { 130 | ranInPhantomRuntime = true; 131 | }); 132 | 133 | let ranInPhantomRuntime = await phantom.windowProperty('ranInPhantomRuntime'); 134 | 135 | expect(ranInPhantomRuntime).toBeFalsy(); 136 | }); 137 | 138 | it('#on() can register at the same event to run locally or in phantom runtime', async () => { 139 | const page = await phantom.createPage(); 140 | let ranHere = false; 141 | 142 | // eslint-disable-next-line 143 | await page.on('onResourceReceived', true, function() { 144 | runnedInPhantomRuntime = true; 145 | }); 146 | 147 | await page.on('onResourceReceived', () => { 148 | ranHere = true; 149 | }); 150 | 151 | await page.open(`http://localhost:${port}/test`); 152 | 153 | let runnedInPhantomRuntime = await phantom.windowProperty('runnedInPhantomRuntime'); 154 | 155 | expect(ranHere).toBe(true); 156 | expect(runnedInPhantomRuntime).toBe(true); 157 | }); 158 | 159 | it('#off() can disable an event whose listener is going to run locally', async () => { 160 | const page = await phantom.createPage(); 161 | let runnedHere = false; 162 | 163 | await page.on('onResourceReceived', () => { 164 | runnedHere = true; 165 | }); 166 | 167 | await page.off('onResourceReceived'); 168 | 169 | await page.open(`http://localhost:${port}/test`); 170 | 171 | expect(runnedHere).toBe(false); 172 | }); 173 | 174 | it('#off() can disable an event whose listener is going to run on the phantom process', async () => { 175 | const page = await phantom.createPage(); 176 | 177 | // eslint-disable-next-line 178 | await page.on('onResourceReceived', true, function() { 179 | runnedInPhantomRuntime = true; 180 | }); 181 | 182 | await page.off('onResourceReceived'); 183 | 184 | await page.open(`http://localhost:${port}/test`); 185 | 186 | let runnedInPhantomRuntime = await phantom.windowProperty('runnedInPhantomRuntime'); 187 | 188 | expect(runnedInPhantomRuntime).toBeFalsy(); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /src/__tests__/page.test.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import fs from 'fs'; 3 | import 'babel-polyfill'; 4 | import Phantom from '../phantom'; 5 | 6 | describe('Page', () => { 7 | let server; 8 | let phantom; 9 | let port; 10 | beforeAll((done) => { 11 | server = http.createServer((request, response) => { 12 | if (request.url === '/script.js') { 13 | response.end('window.fooBar = 2;'); 14 | } else if (request.url === '/test.html') { 15 | response.end('Page TitleTest'); 16 | } else if (request.url === '/upload.html') { 17 | response.end('Page Title' 18 | + ''); 19 | } else { 20 | response.end(`hi, ${request.url}`); 21 | } 22 | }); 23 | server.listen(0, () => { 24 | port = server.address().port; // eslint-disable-line 25 | done(); 26 | }); 27 | }); 28 | 29 | afterAll(() => server.close()); 30 | beforeEach(() => { 31 | phantom = new Phantom(); 32 | }); 33 | afterEach(() => phantom.exit()); 34 | 35 | it('#open() a valid page', async () => { 36 | const page = await phantom.createPage(); 37 | const status = await page.open(`http://localhost:${port}/test`); 38 | expect(status).toEqual('success'); 39 | }); 40 | 41 | it("#property('plainText') returns valid content", async () => { 42 | const page = await phantom.createPage(); 43 | await page.open(`http://localhost:${port}/test`); 44 | const content = await page.property('plainText'); 45 | expect(content).toEqual('hi, /test'); 46 | }); 47 | 48 | it("#property('onResourceRequested', function(){}, params...) to throw exception", async () => { 49 | const page = await phantom.createPage(); 50 | expect(() => { 51 | page.property( 52 | 'onResourceRequested', 53 | function(requestData, networkRequest, foo, a, b) { // eslint-disable-line 54 | RESULT = [foo, a, b]; // eslint-disable-line 55 | }, 56 | 'foobar', 57 | 1, 58 | -100, 59 | ); 60 | }).toThrow(); 61 | }); 62 | 63 | it("#property('onResourceRequested', function(){}) to throw exception", async () => { 64 | const page = await phantom.createPage(); 65 | expect(() => { 66 | page.property('onResourceRequested', function(){}); // eslint-disable-line 67 | }).toThrow(); 68 | }); 69 | 70 | it("#property('key', value) sets property", async () => { 71 | const page = await phantom.createPage(); 72 | await page.property('viewportSize', { width: 800, height: 600 }); 73 | const value = await page.property('viewportSize'); 74 | expect(value).toEqual({ width: 800, height: 600 }); 75 | }); 76 | 77 | xit("#property('paperSize', value) sets value properly with phantom.paperSize", async () => { 78 | const page = await phantom.createPage(); 79 | page.property('paperSize', { 80 | width: '8.5in', 81 | height: '11in', 82 | header: { 83 | height: '1cm', 84 | contents: phantom.callback((pageNum, numPages) => `

Header ${pageNum} / ${numPages}

`), 85 | }, 86 | footer: { 87 | height: '1cm', 88 | contents: phantom.callback((pageNum, numPages) => `

Footer ${pageNum} / ${numPages}

`), 89 | }, 90 | }); 91 | 92 | await page.open(`http://localhost:${port}/test`); 93 | const file = 'test.pdf'; 94 | await page.render(file); 95 | expect(() => { 96 | fs.accessSync(file, fs.F_OK); 97 | }).not.toThrow(); 98 | fs.unlinkSync(file); 99 | }); 100 | 101 | it("#setting('javascriptEnabled') returns true", async () => { 102 | const page = await phantom.createPage(); 103 | const value = await page.setting('javascriptEnabled'); 104 | expect(value).toBe(true); 105 | }); 106 | 107 | it("#setting('key', value) sets setting", async () => { 108 | const page = await phantom.createPage(); 109 | await page.setting('javascriptEnabled', false); 110 | const value = await page.setting('javascriptEnabled'); 111 | expect(value).toBe(false); 112 | }); 113 | 114 | it('#injectJs() properly injects a js file', async () => { 115 | const page = await phantom.createPage(); 116 | await page.open(`http://localhost:${port}/test`); 117 | // inject_example.js: window.foo = 1; 118 | await page.injectJs(`${__dirname}/inject_example.js`); 119 | const response = await page.evaluate(function(){return foo}); // eslint-disable-line 120 | expect(response).toEqual(1); 121 | }); 122 | 123 | it('#includeJs() properly injects a js file', async () => { 124 | const page = await phantom.createPage(); 125 | await page.open(`http://localhost:${port}/test`); 126 | await page.includeJs(`http://localhost:${port}/script.js`); 127 | const response = await page.evaluate(function(){return fooBar}); // eslint-disable-line 128 | expect(response).toEqual(2); 129 | }); 130 | 131 | it('#render() creates a file', async () => { 132 | const page = await phantom.createPage(); 133 | await page.open(`http://localhost:${port}/test`); 134 | const file = 'test.png'; 135 | await page.render(file); 136 | expect(() => { 137 | fs.accessSync(file, fs.F_OK); 138 | }).not.toThrow(); 139 | fs.unlinkSync(file); 140 | }); 141 | 142 | it('#renderBase64() returns encoded PNG', async () => { 143 | const page = await phantom.createPage(); 144 | await page.open(`http://localhost:${port}/test`); 145 | const content = await page.renderBase64('PNG'); 146 | expect(content).not.toBeNull(); 147 | }); 148 | 149 | it('#addCookie() adds a cookie to the page', async () => { 150 | const page = await phantom.createPage(); 151 | await page.addCookie({ 152 | name: 'Valid-Cookie-Name', 153 | value: 'Valid-Cookie-Value', 154 | domain: 'localhost', 155 | path: '/foo', 156 | httponly: true, 157 | secure: false, 158 | expires: new Date().getTime() + (1000 * 60 * 60), 159 | }); 160 | const cookies = await page.cookies(); 161 | expect(cookies[0].name).toEqual('Valid-Cookie-Name'); 162 | }); 163 | 164 | it('#clearCookies() removes all cookies', async () => { 165 | const page = await phantom.createPage(); 166 | 167 | // Probably not the best test if this method doesn't work 168 | await page.addCookie({ 169 | name: 'Valid-Cookie-Name', 170 | value: 'Valid-Cookie-Value', 171 | domain: 'localhost', 172 | path: '/foo', 173 | httponly: true, 174 | secure: false, 175 | expires: new Date().getTime() + (1000 * 60 * 60), 176 | }); 177 | 178 | await page.clearCookies(); 179 | const cookies = await page.cookies(); 180 | expect(cookies).toEqual([]); 181 | }); 182 | 183 | it('#deleteCookie() removes one cookie', async () => { 184 | const page = await phantom.createPage(); 185 | 186 | // Probably not the best test if this method doesn't work 187 | await page.addCookie({ 188 | name: 'cookie-1', 189 | value: 'Valid-Cookie-Value', 190 | domain: 'localhost', 191 | path: '/foo', 192 | httponly: true, 193 | secure: false, 194 | expires: new Date().getTime() + (1000 * 60 * 60), 195 | }); 196 | 197 | await page.addCookie({ 198 | name: 'cookie-2', 199 | value: 'Valid-Cookie-Value', 200 | domain: 'localhost', 201 | path: '/foo', 202 | httponly: true, 203 | secure: false, 204 | expires: new Date().getTime() + (1000 * 60 * 60), 205 | }); 206 | 207 | let cookies = await page.cookies(); 208 | expect(cookies.length).toBe(2); 209 | 210 | await page.deleteCookie('cookie-1'); 211 | cookies = await page.cookies(); 212 | 213 | expect(cookies.length).toBe(1); 214 | expect(cookies[0].name).toEqual('cookie-2'); 215 | }); 216 | 217 | it('#reject(...) works when there is an error', async () => { 218 | try { 219 | await phantom.execute('page', 'nonexistentCommand'); 220 | } catch (e) { 221 | expect(e.message).toEqual("'nonexistentCommand' isn't a command."); 222 | } 223 | }); 224 | 225 | it('#open opens multiple pages', async () => { 226 | const page1 = await phantom.createPage(); 227 | await page1.open(`http://localhost:${port}/test1`); 228 | page1.close(); 229 | 230 | const page2 = await phantom.createPage(); 231 | await page2.open(`http://localhost:${port}/test2`); 232 | const content = await page2.property('plainText'); 233 | expect(content).toEqual('hi, /test2'); 234 | page2.close(); 235 | }); 236 | 237 | it('#windowProperty() returns a window value', async () => { 238 | const page = await phantom.createPage(); 239 | 240 | await page.on('onResourceReceived', true, function(response) { // eslint-disable-line 241 | lastResponse = response; // eslint-disable-line 242 | }); 243 | await page.open(`http://localhost:${port}/test`); 244 | let lastResponse = await phantom.windowProperty('lastResponse'); 245 | expect(lastResponse.url).toEqual(`http://localhost:${port}/test`); 246 | }); 247 | 248 | it('#setContent() works with custom url', async () => { 249 | const page = await phantom.createPage(); 250 | const html = 'setContent Title'; 251 | 252 | await page.setContent(html, `http://localhost:${port}/`); 253 | 254 | // eslint-disable-next-line 255 | const response = await page.evaluate(function() {return [document.title, window.location.href]}); 256 | 257 | expect(response).toEqual(['setContent Title', `http://localhost:${port}/`]); 258 | }); 259 | 260 | it('#sendEvent() sends an event', async () => { 261 | const page = await phantom.createPage(); 262 | const html = 'setContent Title' 263 | + ''; 264 | 265 | await page.setContent(html, `http://localhost:${port}/`); 266 | await page.sendEvent('click', 1, 2); 267 | 268 | const response = await page.evaluate(function() {return window.docClicked}); // eslint-disable-line 269 | 270 | expect(response).toBe(true); 271 | }); 272 | 273 | it('#switchToFrame(framePosition) will switch to frame of framePosition', async () => { 274 | const page = await phantom.createPage(); 275 | const html = 'Iframe Test' 276 | + ``; 277 | 278 | await page.setContent(html, `http://localhost:${port}/`); 279 | await page.switchToFrame(0); 280 | 281 | // eslint-disable-next-line 282 | const inIframe = await page.evaluate(function() {return window.frameElement && window.frameElement.id === 'testframe'}); 283 | expect(inIframe).toBe(true); 284 | }); 285 | 286 | it('#switchToMainFrame() will switch back to the main frame', async () => { 287 | const page = await phantom.createPage(); 288 | const html = 'Iframe Test' 289 | + ``; 290 | 291 | await page.setContent(html, `http://localhost:${port}/`); 292 | // need to switch to child frame here to test switchToMainFrame() works 293 | await page.switchToFrame(0); 294 | await page.switchToMainFrame(); 295 | 296 | // eslint-disable-next-line 297 | const inMainFrame = await page.evaluate(function(){return !window.frameElement}); 298 | 299 | expect(inMainFrame).toBe(true); 300 | }); 301 | 302 | it('#reload() will reload the current page', async () => { 303 | const page = await phantom.createPage(); 304 | let reloaded = false; 305 | 306 | await page.open(`http://localhost:${port}/test`); 307 | await page.on('onNavigationRequested', (url, type) => { 308 | if (type === 'Reload') { 309 | reloaded = true; 310 | } 311 | }); 312 | await page.reload(); 313 | 314 | expect(reloaded).toBe(true); 315 | }); 316 | 317 | it("#invokeAsyncMethod('includeJs', 'http://localhost:port/script.js') executes correctly", async () => { 318 | const page = await phantom.createPage(); 319 | await page.open(`http://localhost:${port}/test`); 320 | await page.invokeAsyncMethod('includeJs', `http://localhost:${port}/script.js`); 321 | const response = await page.evaluate(function() {return fooBar}); // eslint-disable-line 322 | expect(response).toEqual(2); 323 | }); 324 | 325 | it("#invokeAsyncMethod('open', 'http://localhost:port/test') executes correctly", async () => { 326 | const page = await phantom.createPage(); 327 | const status = await page.invokeAsyncMethod('open', `http://localhost:${port}/test`); 328 | expect(status).toEqual('success'); 329 | }); 330 | 331 | it("#invokeMethod('evaluate', 'function () { return document.title }') executes correctly", async () => { 332 | const page = await phantom.createPage(); 333 | await page.open(`http://localhost:${port}/test.html`); 334 | const response = await page.invokeMethod('evaluate', 'function () { return document.title }'); 335 | expect(response).toEqual('Page Title'); 336 | }); 337 | 338 | it("#invokeMethod('renderBase64') executes correctly", async () => { 339 | const page = await phantom.createPage(); 340 | await page.open(`http://localhost:${port}/test`); 341 | const content = await page.invokeMethod('renderBase64', 'PNG'); 342 | expect(content).not.toBeNull(); 343 | }); 344 | 345 | it('#defineMethod(name, definition) defines a method', async () => { 346 | const page = await phantom.createPage(); 347 | await page.defineMethod('getZoomFactor', function zoom() { 348 | return this.zoomFactor; // eslint-disable-line no-invalid-this 349 | }); 350 | const zoomFactor = await page.invokeMethod('getZoomFactor'); 351 | expect(zoomFactor).toEqual(1); 352 | }); 353 | 354 | it('#openUrl() opens a URL', (done) => { 355 | phantom.createPage().then((page) => { 356 | page.on('onLoadFinished', false, (status) => { 357 | expect(status).toEqual('success'); 358 | done(); 359 | }); 360 | return page.openUrl(`http://localhost:${port}/test`, 'GET', {}); 361 | }); 362 | }); 363 | 364 | it('#setProxy() sets the proxy', async () => { 365 | const page = await phantom.createPage(); 366 | await page.setProxy(`http://localhost:${port}/`); 367 | await page.open('http://phantomjs.org/'); 368 | const text = await page.property('plainText'); 369 | expect(text).toEqual('hi, http://phantomjs.org/'); 370 | }); 371 | 372 | it('#property = something shows a warning', async () => { 373 | if (typeof Proxy === 'function') { 374 | const logger = { warn: jest.fn() }; 375 | 376 | const pp = new Phantom([], { logger }); 377 | const page = await pp.createPage(); 378 | 379 | try { 380 | page.foo = 'test'; 381 | } catch (e) { 382 | expect(e).toBeInstanceOf(TypeError); 383 | } finally { 384 | expect(logger.warn).toHaveBeenCalled(); 385 | pp.exit(); 386 | } 387 | } 388 | }); 389 | 390 | it('#goBack()', (done) => { 391 | let page; 392 | phantom 393 | .createPage() 394 | .then((instance) => { 395 | page = instance; 396 | return page.open(`http://localhost:${port}/test1`); 397 | }) 398 | .then(() => page.open(`http://localhost:${port}/test2`)) 399 | .then(() => { 400 | page.on('onNavigationRequested', false, () => { 401 | done(); 402 | }); 403 | return page.goBack(); 404 | }); 405 | }); 406 | 407 | it('#uploadFile() inserts file into file input field', async () => { 408 | const page = await phantom.createPage(); 409 | await page.open(`http://localhost:${port}/upload.html`); 410 | await page.uploadFile('#upload', `${process.env.PWD}/package.json`); 411 | 412 | // eslint-disable-next-line 413 | const response = await page.evaluate(function() {return document.querySelector('#upload').files[0].name}); 414 | expect(response).toEqual('package.json'); 415 | }); 416 | }); 417 | -------------------------------------------------------------------------------- /src/__tests__/phantom.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | import phantomjs from 'phantomjs-prebuilt'; 4 | import path from 'path'; 5 | import Phantom from '../phantom'; 6 | import Page from '../page'; 7 | 8 | describe('Phantom', () => { 9 | let instance; 10 | beforeEach(() => jest.resetModules()); 11 | beforeEach(() => { 12 | instance = new Phantom(); 13 | }); 14 | afterEach(async () => instance.exit()); 15 | 16 | it('#createPage() returns a Promise', () => { 17 | const page = instance.createPage(); 18 | expect(page).toBeInstanceOf(Promise); 19 | return page; 20 | }); 21 | 22 | it('#createPage() resolves to a Page', async () => { 23 | const page = await instance.createPage(); 24 | expect(page).toBeInstanceOf(Page); 25 | }); 26 | 27 | it('#create([], {}) execute with no parameters', () => { 28 | jest.mock('child_process'); 29 | 30 | const actualSpawn = require.requireActual('child_process').spawn; 31 | const mockedSpawn = jest.fn((...args) => actualSpawn(...args)); 32 | require('child_process').setMockedSpawn(mockedSpawn); 33 | 34 | const MockedProcess = require('../phantom').default; 35 | 36 | const pp = new MockedProcess(); 37 | pp.exit(); 38 | 39 | const pathToShim = path.normalize(`${__dirname}/../shim/index.js`); 40 | expect(mockedSpawn).toHaveBeenCalledWith(phantomjs.path, [pathToShim], { env: process.env }); 41 | }); 42 | 43 | it('#create(["--ignore-ssl-errors=yes"]) adds parameter to process', () => { 44 | jest.mock('child_process'); 45 | 46 | const actualSpawn = require.requireActual('child_process').spawn; 47 | const mockedSpawn = jest.fn((...args) => actualSpawn(...args)); 48 | require('child_process').setMockedSpawn(mockedSpawn); 49 | 50 | const MockedProcess = require('../phantom').default; 51 | 52 | const pp = new MockedProcess(['--ignore-ssl-errors=yes']); 53 | pp.exit(); 54 | 55 | const pathToShim = path.normalize(`${__dirname}/../shim/index.js`); 56 | const { env } = process; 57 | expect(mockedSpawn).toHaveBeenCalledWith( 58 | phantomjs.path, 59 | ['--ignore-ssl-errors=yes', pathToShim], 60 | { env }, 61 | ); 62 | }); 63 | 64 | it("#create([], {phantomPath: 'phantomjs'}) execute phantomjs from custom path with no parameters", () => { 65 | jest.mock('child_process'); 66 | 67 | const actualSpawn = require.requireActual('child_process').spawn; 68 | const mockedSpawn = jest.fn((...args) => actualSpawn(...args)); 69 | require('child_process').setMockedSpawn(mockedSpawn); 70 | 71 | const MockedProcess = require('../phantom').default; 72 | 73 | const pp = new MockedProcess([], { phantomPath: 'phantomjs' }); 74 | pp.exit(); 75 | 76 | const pathToShim = path.normalize(`${__dirname}/../shim/index.js`); 77 | expect(mockedSpawn).toHaveBeenCalledWith('phantomjs', [pathToShim], { env: process.env }); 78 | pp.exit(); 79 | }); 80 | 81 | it('#create([], {logger: logger}) to log messages', () => { 82 | const logger = { debug: jest.fn() }; 83 | 84 | const pp = new Phantom([], { logger }); 85 | expect(logger.debug).toHaveBeenCalled(); 86 | pp.exit(); 87 | }); 88 | 89 | it.skip("#create([], {logLevel: 'debug'}) change logLevel", () => { 90 | const logLevel = 'error'; 91 | 92 | const pp = new Phantom([], { logLevel }); 93 | expect(pp.logger.transports.console.level).toEqual(logLevel); 94 | pp.exit(); 95 | }); 96 | 97 | it.skip("#create([], {logLevel: 'debug'}) should not change other log levels", () => { 98 | const logLevel = 'error'; 99 | const p1 = new Phantom([], { logLevel }); 100 | p1.exit(); 101 | 102 | const p2 = new Phantom(); 103 | expect(p2.logger.transports.console.level).toEqual('info'); 104 | p2.exit(); 105 | }); 106 | 107 | it('#create("--ignore-ssl-errors=yes") to throw an exception', () => { 108 | expect(() => new Phantom('--ignore-ssl-errors=yes')).toThrow(); 109 | }); 110 | 111 | it('#create(true) to throw an exception', () => { 112 | expect(() => new Phantom(true)).toThrow(); 113 | }); 114 | 115 | xit('catches errors when stdin closes unexpectedly', async () => { 116 | instance.process.stdin.end(); 117 | await expect(instance.createPage()).rejects.toEqual({ 118 | error: 'Error reading from stdin: Error: write after end', 119 | }); 120 | }); 121 | 122 | xit('catches errors when stdout closes unexpectedly', async () => { 123 | instance.process.stdout.end(); 124 | try { 125 | await expect(instance.createPage()).rejects.toEqual(); 126 | } catch (e) { 127 | expect(e).toEqual(new Error('Error reading from stdout: Error: shutdown ENOTCONN')); 128 | } 129 | }); 130 | 131 | it('#cookies() should return an empty cookies array', async () => { 132 | const cookies = await instance.cookies(); 133 | expect(cookies).toEqual([]); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/command.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * A simple command class that gets deserialized when it is sent to phantom 5 | */ 6 | let NEXT_ID: number = 1; 7 | 8 | export default class Command { 9 | id: number; 10 | 11 | target: string; 12 | 13 | name: string; 14 | 15 | params: mixed[]; 16 | 17 | deferred: ?{ resolve: Function, reject: Function }; 18 | 19 | constructor(target: string, name: string, params: mixed[] = []) { 20 | this.id = NEXT_ID; 21 | NEXT_ID += 1; 22 | this.target = target; 23 | this.name = name; 24 | this.params = params; 25 | this.deferred = null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Phantom from './phantom'; 4 | 5 | /** 6 | * Returns a Promise of a new Phantom class instance 7 | * @param args command args to pass to phantom process 8 | * @param [config] configuration object 9 | * @param [config.phantomPath] path to phantomjs executable 10 | * @param [config.logger] object containing functions used for logging 11 | * @param [config.logLevel] log level to apply on the logger (if unset or default) 12 | * @returns {Promise} 13 | */ 14 | function create(args?: string[] = ['--local-url-access=false'], config?: Config): Promise { 15 | return new Promise(resolve => resolve(new Phantom(args, config))); 16 | } 17 | 18 | module.exports = { create }; 19 | -------------------------------------------------------------------------------- /src/out_object.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import crypto from 'crypto'; 3 | import Phantom from './phantom'; 4 | 5 | // TODO deprecate this 6 | export default class OutObject { 7 | $phantom: Phantom; 8 | 9 | target: string; 10 | 11 | constructor(phantom: Phantom) { 12 | this.$phantom = phantom; 13 | this.target = `OutObject$${crypto.randomBytes(16).toString('hex')}`; 14 | } 15 | 16 | property(name: string) { 17 | return this.$phantom.execute(this.target, 'property', [name]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/page.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Phantom from './phantom'; 4 | 5 | /** 6 | * Page class that proxies everything to phantomjs 7 | */ 8 | export default class Page { 9 | $phantom: Phantom; 10 | 11 | target: string; 12 | 13 | constructor(phantom: Phantom, pageId: string) { 14 | this.target = `page$${pageId}`; 15 | this.$phantom = phantom; 16 | } 17 | 18 | /** 19 | * Add an event listener to the page on phantom 20 | * 21 | * @param event The name of the event (Ej. onResourceLoaded) 22 | * @param [runOnPhantom=false] Indicate if the event must run on the phantom runtime or not 23 | * @param listener The event listener. When runOnPhantom=true, this listener code would be 24 | * run on phantom, and thus, all the closure info wont work 25 | * @returns {*} 26 | */ 27 | on(event: string, runOnPhantom: boolean = false, listener: Function, ...args: any[]) { 28 | let mustRunOnPhantom; 29 | let callback; 30 | let params; 31 | 32 | if (typeof runOnPhantom === 'function') { 33 | mustRunOnPhantom = false; 34 | params = [listener, ...args]; 35 | callback = runOnPhantom.bind(this); 36 | } else { 37 | params = args; 38 | mustRunOnPhantom = runOnPhantom; 39 | callback = mustRunOnPhantom ? listener : listener.bind(this); 40 | } 41 | 42 | return this.$phantom.on(event, this.target, mustRunOnPhantom, callback, params); 43 | } 44 | 45 | /** 46 | * Removes an event listener 47 | * 48 | * @param event the event name 49 | * @returns {*} 50 | */ 51 | off(event: string) { 52 | return this.$phantom.off(event, this.target); 53 | } 54 | 55 | /** 56 | * Invokes an asynchronous method 57 | */ 58 | invokeAsyncMethod(...args: any[]) { 59 | return this.$phantom.execute(this.target, 'invokeAsyncMethod', args); 60 | } 61 | 62 | /** 63 | * Invokes a method 64 | */ 65 | invokeMethod(...args: any[]) { 66 | return this.$phantom.execute(this.target, 'invokeMethod', args); 67 | } 68 | 69 | /** 70 | * Defines a method 71 | */ 72 | defineMethod(name: string, definition: Function) { 73 | return this.$phantom.execute(this.target, 'defineMethod', [name, definition]); 74 | } 75 | 76 | /** 77 | * Gets or sets a property 78 | */ 79 | property(...args: any[]): Promise<*> { 80 | if (args.length > 1 && typeof args[1] === 'function') { 81 | throw new Error('page.property(key, function(){}) has been removed. Use page.on(key, function(){}) instead.'); 82 | } 83 | return this.$phantom.execute(this.target, 'property', args); 84 | } 85 | 86 | /** 87 | * Gets or sets a setting 88 | */ 89 | setting(...args: any[]): Promise<*> { 90 | return this.$phantom.execute(this.target, 'setting', args); 91 | } 92 | 93 | cookies(): Promise<*> { 94 | return this.property('cookies'); 95 | } 96 | } 97 | 98 | const asyncMethods = ['includeJs', 'open']; 99 | 100 | const methods = [ 101 | 'addCookie', 102 | 'clearCookies', 103 | 'close', 104 | 'deleteCookie', 105 | 'evaluate', 106 | 'evaluateAsync', 107 | 'evaluateJavaScript', 108 | 'injectJs', 109 | 'openUrl', 110 | 'reload', 111 | 'render', 112 | 'renderBase64', 113 | 'sendEvent', 114 | 'setContent', 115 | 'setProxy', 116 | 'stop', 117 | 'switchToFrame', 118 | 'switchToMainFrame', 119 | 'goBack', 120 | 'uploadFile', 121 | ]; 122 | 123 | asyncMethods.forEach((method) => { 124 | // $FlowFixMe: no way to provide dynamic functions 125 | Page.prototype[method] = function _(...args: any[]) { 126 | return this.invokeAsyncMethod.apply(this, [method, ...args]); 127 | }; 128 | }); 129 | 130 | methods.forEach((method) => { 131 | // $FlowFixMe: no way to provide dynamic functions 132 | Page.prototype[method] = function _(...args: any[]) { 133 | return this.invokeMethod.apply(this, [method, ...args]); 134 | }; 135 | }); 136 | -------------------------------------------------------------------------------- /src/phantom.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import phantomjs from 'phantomjs-prebuilt'; 4 | import { spawn } from 'child_process'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | import split from 'split'; 8 | import winston from 'winston'; 9 | import EventEmitter from 'events'; 10 | import Page from './page'; 11 | import Command from './command'; 12 | import OutObject from './out_object'; 13 | 14 | type Response = { pageId: string }; 15 | 16 | const defaultLogLevel = process.env.DEBUG === 'true' ? 'debug' : 'info'; 17 | const defaultPathToShim = path.normalize(`${__dirname}/shim/index.js`); 18 | const NOOP = 'NOOP'; 19 | 20 | /** 21 | * Creates a logger using winston 22 | */ 23 | function createLogger({ logLevel = defaultLogLevel } = {}) { 24 | const { format } = winston; 25 | return winston.createLogger({ 26 | transports: [ 27 | new winston.transports.Console({ 28 | level: logLevel, 29 | format: format.combine(winston.format.colorize(), format.splat(), format.simple()), 30 | }), 31 | ], 32 | }); 33 | } 34 | 35 | /** 36 | * A phantom instance that communicates with phantomjs 37 | */ 38 | export default class Phantom { 39 | logger: Logger; 40 | 41 | isNoOpInProgress: boolean; 42 | 43 | commands: Map; 44 | 45 | events: Map; 46 | 47 | heartBeatId: IntervalID; 48 | 49 | process: child_process$ChildProcess; // eslint-disable-line camelcase 50 | 51 | /** 52 | * Creates a new instance of Phantom 53 | * 54 | * @param args command args to pass to phantom process 55 | * @param [phantomPath] path to phantomjs executable 56 | * @param [logger] object containing functions used for logging 57 | * @param [logLevel] log level to apply on the logger (if unset or default) 58 | */ 59 | // eslint-disable-next-line 60 | constructor( 61 | args?: string[] = [], 62 | { 63 | phantomPath = phantomjs.path, 64 | shimPath = defaultPathToShim, 65 | logLevel = defaultLogLevel, 66 | logger = createLogger({ logLevel }), 67 | }: Config = {}, 68 | ) { 69 | if (!Array.isArray(args)) { 70 | throw new Error('Unexpected type of parameters. Expecting args to be array.'); 71 | } 72 | 73 | if (typeof phantomPath !== 'string') { 74 | throw new Error('PhantomJS binary was not found. ' 75 | + 'This generally means something went wrong when installing phantomjs-prebuilt. Exiting.'); 76 | } 77 | 78 | if (typeof shimPath !== 'string') { 79 | throw new Error('Path to shim file was not found. ' 80 | + 'Are you sure you entered the path correctly? Exiting.'); 81 | } 82 | 83 | if (!logger.info && !logger.debug && !logger.error && !logger.warn) { 84 | throw new Error('logger must be a valid object.'); 85 | } 86 | 87 | // eslint-disable-next-line no-unused-vars 88 | const noop = (...msg) => undefined; 89 | 90 | this.logger = { 91 | info: logger.info ? (...msg) => logger.info(...msg) : noop, 92 | debug: logger.debug ? (...msg) => logger.debug(...msg) : noop, 93 | error: logger.error ? (...msg) => logger.error(...msg) : noop, 94 | warn: logger.warn ? (...msg) => logger.warn(...msg) : noop, 95 | }; 96 | 97 | this.logger.debug(`Starting ${phantomPath} ${args.concat([shimPath]).join(' ')}`); 98 | 99 | this.process = spawn(phantomPath, args.concat([shimPath]), { env: process.env }); 100 | this.process.stdin.setDefaultEncoding('utf-8'); 101 | 102 | this.commands = new Map(); 103 | this.events = new Map(); 104 | 105 | this.process.stdout.pipe(split()).on('data', (data) => { 106 | const message = data.toString('utf8'); 107 | if (message[0] === '>') { 108 | // Server end has finished NOOP, lets allow NOOP again.. 109 | if (message === `>${NOOP}`) { 110 | this.logger.debug('Received NOOP command.'); 111 | this.isNoOpInProgress = false; 112 | return; 113 | } 114 | const json = message.substr(1); 115 | this.logger.debug('Parsing: %s', json); 116 | 117 | const parsedJson = JSON.parse(json); 118 | const command = this.commands.get(parsedJson.id); 119 | 120 | if (command != null) { 121 | const { deferred } = command; 122 | 123 | if (deferred != null) { 124 | if (parsedJson.error === undefined) { 125 | deferred.resolve(parsedJson.response); 126 | } else { 127 | deferred.reject(new Error(parsedJson.error)); 128 | } 129 | } else { 130 | this.logger.error(`deferred object not found for command.id: ${parsedJson.id}`); 131 | } 132 | 133 | this.commands.delete(command.id); 134 | } else { 135 | this.logger.error(`command not found for command.id: ${parsedJson.id}`); 136 | } 137 | } else if (message.indexOf('') === 0) { 138 | const json = message.substr(7); 139 | this.logger.debug('Parsing: %s', json); 140 | const event = JSON.parse(json); 141 | 142 | const emitter = this.events.get(event.target); 143 | if (emitter) { 144 | emitter.emit(...[event.type].concat(event.args)); 145 | } 146 | } else if (message && message.length > 0) { 147 | this.logger.info(message); 148 | } 149 | }); 150 | 151 | this.process.stderr.on('data', data => this.logger.error(data.toString('utf8'))); 152 | this.process.on('exit', (code) => { 153 | this.logger.debug(`Child exited with code {${code}}`); 154 | this.rejectAllCommands(`Phantom process stopped with exit code ${code}`); 155 | }); 156 | this.process.on('error', (error) => { 157 | this.logger.error(`Could not spawn [${phantomPath}] executable. ` 158 | + 'Please make sure phantomjs is installed correctly.'); 159 | this.logger.error(error); 160 | this.kill(`Process got an error: ${error}`); 161 | process.exit(1); 162 | }); 163 | 164 | this.process.stdin.on('error', (e) => { 165 | this.logger.debug(`Child process received error ${e}, sending kill signal`); 166 | this.kill(`Error reading from stdin: ${e}`); 167 | }); 168 | 169 | this.process.stdout.on('error', (e) => { 170 | this.logger.debug(`Child process received error ${e}, sending kill signal`); 171 | this.kill(`Error reading from stdout: ${e}`); 172 | }); 173 | 174 | this.heartBeatId = setInterval(this.heartBeat.bind(this), 100); 175 | } 176 | 177 | /** 178 | * Returns a value in the global space of phantom process 179 | */ 180 | windowProperty(...args: mixed[]): Promise { 181 | return this.execute('phantom', 'windowProperty', args); 182 | } 183 | 184 | /** 185 | * Returns a new instance of Promise which resolves to a {@link Page}. 186 | * @returns {Promise.} 187 | */ 188 | createPage(): Promise { 189 | const { logger } = this; 190 | return this.execute('phantom', 'createPage').then((response: Response) => { 191 | let page = new Page(this, response.pageId); 192 | if (typeof Proxy !== 'function') { 193 | throw new Error('Expected object Proxy to be defined. Make sure you are using Node 6+.'); 194 | } 195 | page = new Proxy(page, { 196 | set(target, prop) { 197 | logger.warn(`Using page.${prop} = ...; is not supported. Use page.property('${prop}', ...) ` 198 | + 'instead. See the README file for more examples of page#property.'); 199 | return false; 200 | }, 201 | }); 202 | return page; 203 | }); 204 | } 205 | 206 | /** 207 | * Creates a special object that can be used for returning data back from PhantomJS 208 | * @returns {OutObject} 209 | */ 210 | createOutObject(): OutObject { 211 | return new OutObject(this); 212 | } 213 | 214 | /** 215 | * Used for creating a callback in phantomjs for content header and footer 216 | * @param obj 217 | */ 218 | // eslint-disable-next-line class-methods-use-this 219 | callback(obj: Function): { transform: true, target: Function, method: 'callback', parent: 'phantom' } { 220 | return { 221 | transform: true, 222 | target: obj, 223 | method: 'callback', 224 | parent: 'phantom', 225 | }; 226 | } 227 | 228 | /** 229 | * Executes a command object 230 | * @param command the command to run 231 | * @returns {Promise} 232 | */ 233 | executeCommand(command: Command): Promise { 234 | this.commands.set(command.id, command); 235 | 236 | const json = JSON.stringify(command, (key, val) => { 237 | if (key[0] === '$') { 238 | // if key starts with $ then ignore because it's private 239 | return undefined; 240 | } if (typeof val === 'function') { 241 | if (!Object.prototype.hasOwnProperty.call(val, 'prototype')) { 242 | this.logger.warn('Arrow functions such as () => {} are not supported in PhantomJS. ' 243 | + 'Please use function(){} or compile to ES5.'); 244 | throw new Error('Arrow functions such as () => {} are not supported in PhantomJS.'); 245 | } 246 | return val.toString(); 247 | } 248 | return val; 249 | }); 250 | 251 | const promise = new Promise((res, rej) => { 252 | command.deferred = { resolve: res, reject: rej }; // eslint-disable-line no-param-reassign 253 | }); 254 | 255 | this.logger.debug('Sending: %s', json); 256 | 257 | this.process.stdin.write(json + os.EOL, 'utf8'); 258 | 259 | return promise; 260 | } 261 | 262 | /** 263 | * Executes a command 264 | * 265 | * @param target target object to execute against 266 | * @param name the name of the method execute 267 | * @param args an array of args to pass to the method 268 | * @returns {Promise} 269 | */ 270 | execute(target: string, name: string, args: mixed[] = []): Promise { 271 | return this.executeCommand(new Command(target, name, args)); 272 | } 273 | 274 | /** 275 | * Adds an event listener to a target object (currently only works on pages) 276 | * 277 | * @param event the event type 278 | * @param target target object to execute against 279 | * @param runOnPhantom would the callback run in phantomjs or not 280 | * @param callback the event callback 281 | * @param args an array of args to pass to the callback 282 | */ 283 | on(event: string, target: string, runOnPhantom: boolean, callback: Function, args: mixed[] = []) { 284 | const eventDescriptor: { type: string, args?: mixed[], event?: Function } = { type: event }; 285 | 286 | if (runOnPhantom) { 287 | eventDescriptor.event = callback; 288 | eventDescriptor.args = args; 289 | } else { 290 | const emitter = this.getEmitterForTarget(target); 291 | emitter.on(event, (...eArgs) => { 292 | const params = eArgs.concat(args); 293 | return callback(...params); 294 | }); 295 | } 296 | return this.execute(target, 'addEvent', [eventDescriptor]); 297 | } 298 | 299 | /** 300 | * Removes an event from a target object 301 | * 302 | * @param event 303 | * @param target 304 | */ 305 | off(event: string, target: string): Promise { 306 | const emitter = this.getEmitterForTarget(target); 307 | emitter.removeAllListeners(event); 308 | return this.execute(target, 'removeEvent', [{ type: event }]); 309 | } 310 | 311 | getEmitterForTarget(target: string): EventEmitter { 312 | let emitter = this.events.get(target); 313 | 314 | if (emitter == null) { 315 | emitter = new EventEmitter(); 316 | this.events.set(target, emitter); 317 | } 318 | 319 | return emitter; 320 | } 321 | 322 | cookies(): Promise<*> { 323 | return this.execute('phantom', 'property', ['cookies']); 324 | } 325 | 326 | /** 327 | * Cleans up and end the phantom process 328 | */ 329 | exit(): Promise { 330 | clearInterval(this.heartBeatId); 331 | if (this.commands.size > 0) { 332 | this.logger.warn('exit() was called before waiting for commands to finish. ' 333 | + 'Make sure you are not calling exit() prematurely.'); 334 | } 335 | return this.execute('phantom', 'invokeMethod', ['exit']); 336 | } 337 | 338 | /** 339 | * Clean up and force kill this process 340 | */ 341 | kill(errmsg: string = 'Phantom process was killed'): void { 342 | this.rejectAllCommands(errmsg); 343 | this.process.kill('SIGKILL'); 344 | } 345 | 346 | heartBeat(): void { 347 | if (!this.isNoOpInProgress) { 348 | this.isNoOpInProgress = true; 349 | this.logger.debug('Sending NOOP command.'); 350 | this.process.stdin.write(NOOP + os.EOL, 'utf8'); 351 | } 352 | } 353 | 354 | /** 355 | * rejects all commands in this.commands 356 | */ 357 | rejectAllCommands(msg: string = 'Phantom exited prematurely'): void { 358 | // prevent heartbeat from preventing this from terminating 359 | clearInterval(this.heartBeatId); 360 | 361 | this.commands.forEach((command) => { 362 | const { params: [name] } = command; 363 | if (name !== 'exit' && command.deferred) { 364 | command.deferred.reject(new Error(msg)); 365 | } 366 | }); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/shim/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/shim/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, import/no-unresolved, 2 | import/no-extraneous-dependencies, import/extensions */ 3 | 4 | import webpage from 'webpage'; 5 | import system from 'system'; 6 | 7 | /** 8 | * Stores all all pages and single instance of phantom 9 | */ 10 | const objectSpace = { 11 | phantom, 12 | }; 13 | 14 | const events = {}; 15 | const NOOP = 'NOOP'; 16 | 17 | /** 18 | * Looks for transform key and uses objectSpace to call objects 19 | * @param object 20 | */ 21 | function transform(object) { 22 | // eslint-disable-next-line no-restricted-syntax 23 | for (const key in object) { 24 | // eslint-disable-next-line no-prototype-builtins 25 | if (object.hasOwnProperty(key)) { 26 | const child = object[key]; 27 | if (child === null || child === undefined) { 28 | return; 29 | } if (child.transform === true) { 30 | object[key] = objectSpace[child.parent][child.method](child.target); 31 | } else if (typeof child === 'object') { 32 | transform(child); 33 | } 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Completes a command by return a response to node and listening again for next command. 40 | * @param command 41 | */ 42 | function completeCommand(command) { 43 | system.stdout.writeLine(`>${JSON.stringify(command)}`); 44 | } 45 | 46 | /** 47 | * Sync all OutObjects present in the array 48 | * 49 | * @param objects 50 | */ 51 | function syncOutObjects(objects) { 52 | objects.forEach((param) => { 53 | if (param.target !== undefined) { 54 | objectSpace[param.target] = param; 55 | } 56 | }); 57 | } 58 | 59 | /** 60 | * Determines a targets type using its id 61 | * 62 | * @param target 63 | * @returns {*} 64 | */ 65 | function getTargetType(target) { 66 | return target.toString().split('$')[0]; 67 | } 68 | 69 | /** 70 | * Verifies if an event is supported for a type of target 71 | * 72 | * @param type 73 | * @param eventName 74 | * @returns {boolean} 75 | */ 76 | function isEventSupported(type, eventName) { 77 | return type === 'page' && eventName.indexOf('on') === 0; 78 | } 79 | 80 | /** 81 | * Returns a function that will notify to node that an event have been triggered 82 | * 83 | * @param eventName 84 | * @param targetId 85 | * @returns {Function} 86 | */ 87 | function getOutsideListener(eventName, targetId) { 88 | return (...args) => { 89 | system.stdout.writeLine(`${JSON.stringify({ target: targetId, type: eventName, args })}`); 90 | }; 91 | } 92 | 93 | /** 94 | * Executes all the listeners for an event from a target 95 | * 96 | * @param target 97 | * @param eventName 98 | */ 99 | function triggerEvent(target, eventName, ...args) { 100 | const listeners = events[target][eventName]; 101 | listeners.outsideListener.apply(null, args); 102 | listeners.otherListeners.forEach((listener) => { 103 | listener.apply(objectSpace[target], args); 104 | }); 105 | } 106 | /** 107 | * Gets an object containing all the listeners for an event of a target 108 | * 109 | * @param target the target id 110 | * @param eventName the event name 111 | */ 112 | function getEventListeners(target, eventName) { 113 | if (!events[target]) { 114 | events[target] = {}; 115 | } 116 | 117 | if (!events[target][eventName]) { 118 | events[target][eventName] = { 119 | outsideListener: getOutsideListener(eventName, target), 120 | otherListeners: [], 121 | }; 122 | 123 | objectSpace[target][eventName] = triggerEvent.bind(null, target, eventName); 124 | } 125 | 126 | return events[target][eventName]; 127 | } 128 | 129 | /** 130 | * All commands that have a custom implementation 131 | */ 132 | const commands = { 133 | createPage: (command) => { 134 | const page = webpage.create(); 135 | objectSpace[`page$${command.id}`] = page; 136 | 137 | page.onClosing = () => delete objectSpace[`page$${command.id}`]; 138 | 139 | command.response = { pageId: command.id }; 140 | completeCommand(command); 141 | }, 142 | property: (command) => { 143 | if (command.params.length > 1) { 144 | if (typeof command.params[1] === 'function') { 145 | // If the second parameter is a function then we want to proxy and pass parameters too 146 | const callback = command.params[1]; 147 | const otherArgs = command.params.slice(2); 148 | syncOutObjects(otherArgs); 149 | // eslint-disable-next-line 150 | objectSpace[command.target][command.params[0]] = (...args) => callback.apply(objectSpace[command.target], args.concat(otherArgs)); 151 | } else { 152 | // If the second parameter is not a function then just assign 153 | const { target, params: [name, value] } = command; 154 | objectSpace[target][name] = value; 155 | } 156 | } else { 157 | command.response = objectSpace[command.target][command.params[0]]; 158 | } 159 | 160 | completeCommand(command); 161 | }, 162 | setting: (command) => { 163 | if (command.params.length === 2) { 164 | const { target, params: [name, value] } = command; 165 | objectSpace[target].settings[name] = value; 166 | } else { 167 | command.response = objectSpace[command.target].settings[command.params[0]]; 168 | } 169 | 170 | completeCommand(command); 171 | }, 172 | 173 | windowProperty: (command) => { 174 | if (command.params.length === 2) { 175 | const { params: [name, value] } = command; 176 | window[name] = value; 177 | } else { 178 | command.response = window[command.params[0]]; 179 | } 180 | completeCommand(command); 181 | }, 182 | 183 | addEvent: (command) => { 184 | const type = getTargetType(command.target); 185 | 186 | if (isEventSupported(type, command.params[0].type)) { 187 | const listeners = getEventListeners(command.target, command.params[0].type); 188 | 189 | if (typeof command.params[0].event === 'function') { 190 | listeners.otherListeners.push((...args) => { 191 | const params = args.concat(command.params[0].args); 192 | return command.params[0].event.apply(objectSpace[command.target], params); 193 | }); 194 | } 195 | } 196 | 197 | completeCommand(command); 198 | }, 199 | 200 | removeEvent(command) { 201 | const type = getTargetType(command.target); 202 | 203 | if (isEventSupported(type, command.params[0].type)) { 204 | events[command.target][command.params[0].type] = null; 205 | objectSpace[command.target][command.params[0].type] = null; 206 | } 207 | 208 | completeCommand(command); 209 | }, 210 | 211 | noop: command => completeCommand(command), 212 | 213 | invokeAsyncMethod(command) { 214 | const target = objectSpace[command.target]; 215 | target[command.params[0]](...command.params.slice(1).concat((result) => { 216 | command.response = result; 217 | completeCommand(command); 218 | })); 219 | }, 220 | 221 | invokeMethod(command) { 222 | const target = objectSpace[command.target]; 223 | const method = target[command.params[0]]; 224 | command.response = method.apply(target, command.params.slice(1)); 225 | completeCommand(command); 226 | }, 227 | 228 | defineMethod(command) { 229 | const target = objectSpace[command.target]; 230 | const { params: [name, value] } = command; 231 | target[name] = value; 232 | completeCommand(command); 233 | }, 234 | }; 235 | 236 | /** 237 | * Executes a command. 238 | * @param command the command to execute 239 | */ 240 | function executeCommand(command) { 241 | if (commands[command.name]) { 242 | return commands[command.name](command); 243 | } 244 | throw new Error(`'${command.name}' isn't a command.`); 245 | } 246 | 247 | /** 248 | * Calls readLine() and blocks until a message is ready 249 | */ 250 | function read() { 251 | const line = system.stdin.readLine(); 252 | if (line) { 253 | if (line === NOOP) { 254 | system.stdout.writeLine(`>${NOOP}`); 255 | setTimeout(read, 100); 256 | return; 257 | } 258 | const command = JSON.parse(line, (key, value) => { 259 | if ( 260 | value 261 | && typeof value === 'string' 262 | && value.substr(0, 8) === 'function' 263 | && value.indexOf('[native code]') === -1 264 | ) { 265 | const startBody = value.indexOf('{') + 1; 266 | const endBody = value.lastIndexOf('}'); 267 | const startArgs = value.indexOf('(') + 1; 268 | const endArgs = value.indexOf(')'); 269 | 270 | // eslint-disable-next-line no-new-func 271 | return new Function( 272 | value.substring(startArgs, endArgs), 273 | value.substring(startBody, endBody), 274 | ); 275 | } 276 | return value; 277 | }); 278 | 279 | // Call here to look for transform key 280 | transform(command.params); 281 | 282 | try { 283 | executeCommand(command); 284 | } catch (e) { 285 | command.error = e.message; 286 | completeCommand(command); 287 | } finally { 288 | setTimeout(read, 0); 289 | } 290 | } 291 | } 292 | 293 | read(); 294 | --------------------------------------------------------------------------------