Repository: schickling/chromeless
Branch: master
Commit: 774223e4e7f6
Files: 40
Total size: 141.0 KB
Directory structure:
gitextract_s2_draj1/
├── .circleci/
│ └── config.yml
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs/
│ └── api.md
├── examples/
│ ├── extract-google-results.js
│ ├── google-pdf.js
│ ├── google-screenshot.js
│ ├── mocha-chai-test-example.js
│ ├── mouse-event-example.js
│ └── twitter.js
├── package.json
├── serverless/
│ ├── README.md
│ ├── package.json
│ ├── serverless.yml
│ ├── src/
│ │ ├── disconnect.ts
│ │ ├── run.ts
│ │ ├── session.ts
│ │ ├── utils.ts
│ │ └── version.ts
│ └── tsconfig.json
├── src/
│ ├── __tests__/
│ │ └── test.html
│ ├── api.ts
│ ├── chrome/
│ │ ├── local-runtime.ts
│ │ ├── local.ts
│ │ └── remote.ts
│ ├── index.ts
│ ├── queue.ts
│ ├── types.ts
│ ├── util.test.ts
│ └── util.ts
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
workflows:
version: 2
chromeless:
jobs:
- build_node_6
- build_node_8
- release:
requires:
- build_node_6
- build_node_8
filters:
branches:
only: master
restore_cache: &restore_cache
restore_cache:
keys:
- npm-cache-{{ checksum "package-lock.json" }}
save_cache: &save_cache
save_cache:
key: npm-cache-{{ checksum "package-lock.json" }}
paths:
- ~/.npm
codecov: &codecov
run:
name: Codecov
command: node_modules/.bin/nyc report --reporter=json && bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json
jobs:
# Build, lint, run tests
build_node_6:
working_directory: ~/chromeless
docker:
- image: circleci/node:6
- image: yukinying/chrome-headless-browser
steps:
- run: node -v; npm -v
- checkout
- *restore_cache
- run: npm install
- *save_cache
- run: npm test
- *codecov
build_node_8:
working_directory: ~/chromeless
docker:
- image: circleci/node:8
- image: yukinying/chrome-headless-browser
steps:
- run: node -v; npm -v
- checkout
- *restore_cache
- run: npm install
- *save_cache
- run: npm test
- *codecov
# On master and if tests passed, parse the commit history to see if a new release should happen
# If yes, publish to npm and tag a release on GitHub
release:
working_directory: ~/chromeless
docker:
- image: circleci/node:8
steps:
- run: node -v; npm -v
- checkout
- *restore_cache
- run: npm install
- *save_cache
- run: npm run build
- run: npm run semantic-release
================================================
FILE: .editorconfig
================================================
[*]
indent_size = 2
indent_style = space
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
# This is a (Bug Report / Feature Proposal)
## Description
For bug reports:
* What went wrong?
* What did you expect should have happened?
* What was the config you used?
* What stacktrace or error message from your provider did you see?
For feature proposals:
* What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us.
* If there is a new API method, how would it look
Similar or dependent issues:
* #12345
## Additional Data
* ***Chromeless Version you're using***:
* ***Operating System***:
* ***Stack Trace***:
* ***Error messages***:
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
- [ ] If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes)
- [ ] Make sure all of the significant new logic is covered by tests
- [ ] Rebase your changes on master so that they can be merged easily
- [ ] Make sure all tests and linter rules pass
- [ ] If you've changed APIs, update the documentation in [README](/) and [/api/README](/api/README.md)
================================================
FILE: .gitignore
================================================
node_modules
dist
.idea
*.log
.DS_Store
.serverless
.build
.envrc
.nyc_output/
coverage/
yarn.lock
================================================
FILE: .prettierignore
================================================
.nyc_output/
*.md
dist/
package.json
package-lock.json
serverless/
================================================
FILE: .prettierrc
================================================
{
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
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.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
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.
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.
## Scope
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.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at marco.luethy@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.
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.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to this project
[fork]: https://github.com/graphcool/chromeless/fork
[pr]: https://github.com/graphcool/chromeless/compare
[code-of-conduct]: CODE_OF_CONDUCT.md
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
## Contribution Agreement
As a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code, you (and, if applicable, your employer) are licensing the submitted code to the open source community subject to the MIT license.
## Submitting a pull request
0. [Fork][fork] and clone the repository
0. Create a new branch: `git checkout -b feature/my-new-feature-name`
0. Run `npm install` to make sure you've got the latest dependencies.
0. Make your change
0. Run the unit tests and make sure they pass and have 100% coverage. (`npm test`)
0. Push to your fork and [submit a pull request][pr]
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Make your commit message follow [Conventional Commits](https://conventionalcommits.org/).
- In your pull request description, provide as much detail as possible. This context helps the reviewer to understand the motivation for and impact of the change.
- Make sure that all the unit tests still pass. PRs with failing tests won't be merged.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Contributors et.al.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
*This project is deprecated in favor for [Puppeteer](https://github.com/GoogleChrome/puppeteer).
Thanks to all the contributors who made this project possible.*
# Chromeless
[](https://npmjs.com/package/chromeless)
[](https://npmjs.com/package/chromeless)
[](https://circleci.com/gh/prismagraphql/workflows/chromeless/tree/master)
[](https://codecov.io/gh/prismagraphql/chromeless)
[](https://david-dm.org/prismagraphql/chromeless)
[]()
[](https://github.com/semantic-release/semantic-release)
Chrome automation made simple. Runs locally or headless on AWS Lambda. (**[See Demo](https://chromeless.netlify.com/)**)
## Chromeless can be used to...
* Run 1000s of **browser integration tests in parallel** ⚡️
* Crawl the web & automate screenshots
* Write bots that require a real browser
* *Do pretty much everything you've used __PhantomJS, NightmareJS or Selenium__ for before*
### Examples
* [JSON of Google Results](examples/extract-google-results.js): Google for `chromeless` and get a list of JSON results
* [Screenshot of Google Results](examples/google-screenshot.js): Google for `chromeless` and take a screenshot of the results
* [prep](https://github.com/prismagraphql/prep): Compile-time prerendering for SPA/PWA (like React, Vue...) instead of server-side rendering (SSR)
* *See the full [examples list](/examples) for more*
## ▶️ Try it out
You can try out Chromeless and explore the API in the browser-based **[demo playground](https://chromeless.netlify.com/)** ([source](https://github.com/prismagraphql/chromeless-playground)).
[](https://chromeless.netlify.com/)
## Contents
1. [How it works](#how-it-works)
1. [Installation](#installation)
1. [Usage](#usage)
1. [API Documentation](#api-documentation)
1. [Configuring Development Environment](#configuring-development-environment)
1. [FAQ](#faq)
1. [Contributors](#contributors)
1. [Credits](#credits)
1. [Help & Community](#help-and-community)
## How it works
With Chromeless you can control Chrome (open website, click elements, fill out forms...) using an [elegant API](docs/api.md). This is useful for integration tests or any other scenario where you'd need to script a real browser.
### There are 2 ways to use Chromeless
1. Running Chrome on your local computer
2. Running Chrome on AWS Lambda and controlling it remotely

### 1. Local Setup
For local development purposes where a fast feedback loop is necessary, the easiest way to use Chromeless is by controlling your local Chrome browser. Just follow the [usage guide](#usage) to get started.
### 2. Remote Proxy Setup
You can also run Chrome in [headless-mode](https://developers.google.com/web/updates/2017/04/headless-chrome) on AWS Lambda. This way you can speed up your tests by running them in parallel. (In [Graphcool](https://www.graph.cool/)'s case this decreased test durations from ~20min to a few seconds.)
Chromeless comes out of the box with a remote proxy built-in - the usage stays completely the same. This way you can write and run your tests locally and have them be executed remotely on AWS Lambda. The proxy connects to Lambda through a Websocket connection to forward commands and return the evaluation results.
## Installation
```sh
npm install chromeless
```
### Proxy Setup
The project contains a [Serverless](https://serverless.com/) service for running and driving Chrome remotely on AWS Lambda.
1. Deploy The Proxy service to AWS Lambda. More details [here](serverless#setup)
2. Follow the usage instructions [here](serverless#using-the-proxy).
## Usage
Using Chromeless is similar to other browser automation tools. For example:
```js
const { Chromeless } = require('chromeless')
async function run() {
const chromeless = new Chromeless()
const screenshot = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
.press(13)
.wait('#resultStats')
.screenshot()
console.log(screenshot) // prints local file path or S3 url
await chromeless.end()
}
run().catch(console.error.bind(console))
```
### Local Chrome Usage
To run Chromeless locally, you need a recent version of Chrome or Chrome Canary installed (version 60 or greater). By default, chromeless will start Chrome automatically and will default to the most recent version found on your system if there's multiple. You can override this behavior by starting Chrome yourself, and passing a flag of `launchChrome: false` in the `Chromeless` constructor.
To launch Chrome yourself, and open the port for chromeless, follow this example:
```sh
alias canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
canary --remote-debugging-port=9222
```
Or run Chrome Canary headless-ly:
```sh
canary --remote-debugging-port=9222 --disable-gpu --headless
```
Or run Chrome headless-ly on Windows:
```sh
cd "C:\Program Files (x86)\Google\Chrome\Application"
chrome --remote-debugging-port=9222 --disable-gpu --headless
```
### Proxy Usage
Follow the setup instructions [here](serverless#installation).
Then using Chromeless with the Proxy service is the same as running it locally with the exception of the `remote` option.
Alternatively you can configure the Proxy service's endpoint with environment variables. [Here's how](serverless#using-the-proxy).
```js
const chromeless = new Chromeless({
remote: {
endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',
apiKey: 'your-api-key-here',
},
})
```
## API Documentation
**Chromeless constructor options**
- [`new Chromeless(options: ChromelessOptions)`](docs/api.md#chromeless-constructor-options)
**Chromeless methods**
- [`end()`](docs/api.md#api-end)
**Chrome methods**
- [`goto(url: string, timeout?: number)`](docs/api.md#api-goto)
- [`setUserAgent(useragent: string)`](docs/api.md#api-setUserAgent)
- [`click(selector: string, x?: number, y?: number)`](docs/api.md#api-click)
- [`wait(timeout: number)`](docs/api.md#api-wait-timeout)
- [`wait(selector: string)`](docs/api.md#api-wait-selector)
- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet
- [`clearCache()`](docs/api.md#api-clearcache)
- [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage)
- [`focus(selector: string)`](docs/api.md#api-focus)
- [`press(keyCode: number, count?: number, modifiers?: any)`](docs/api.md#api-press)
- [`type(input: string, selector?: string)`](docs/api.md#api-type)
- [`back()`](docs/api.md#api-back) - Not implemented yet
- [`forward()`](docs/api.md#api-forward) - Not implemented yet
- [`refresh()`](docs/api.md#api-refresh) - Not implemented yet
- [`mousedown(selector: string)`](docs/api.md#api-mousedown)
- [`mouseup(selector: string)`](docs/api.md#api-mouseup)
- [`scrollTo(x: number, y: number)`](docs/api.md#api-scrollto)
- [`scrollToElement(selector: string)`](docs/api.md#api-scrolltoelement)
- [`setHtml(html: string)`](docs/api.md#api-sethtml)
- [`setExtraHTTPHeaders(headers: Headers)`](docs/api.md#api-setextrahttpheaders)
- [`setViewport(options: DeviceMetrics)`](docs/api.md#api-setviewport)
- [`evaluate(fn: (...args: any[]) => void, ...args: any[])`](docs/api.md#api-evaluate)
- [`inputValue(selector: string)`](docs/api.md#api-inputvalue)
- [`exists(selector: string)`](docs/api.md#api-exists)
- [`screenshot(selector: string, options: ScreenshotOptions)`](docs/api.md#api-screenshot)
- [`pdf(options?: PdfOptions)`](docs/api.md#api-pdf)
- [`html()`](docs/api.md#api-html)
- [`cookies()`](docs/api.md#api-cookies)
- [`cookies(name: string)`](docs/api.md#api-cookies-name)
- [`cookies(query: CookieQuery)`](docs/api.md#api-cookies-query) - Not implemented yet
- [`allCookies()`](docs/api.md#api-all-cookies)
- [`setCookies(name: string, value: string)`](docs/api.md#api-setcookies)
- [`setCookies(cookie: Cookie)`](docs/api.md#api-setcookies-one)
- [`setCookies(cookies: Cookie[])`](docs/api.md#api-setcookies-many)
- [`deleteCookies(name: string)`](docs/api.md#api-deletecookies)
- [`clearCookies()`](docs/api.md#api-clearcookies)
- [`clearInput(selector: string)`](docs/api.md#api-clearInput)
- [`setFileInput(selector: string, files: string | string[])`](docs/api.md#api-set-file-input)
## Configuring Development Environment
**Requirements:**
- NodeJS version 8.2 and greater
1) Clone this repository
2) Run `npm install`
3) To build: `npm run build`
#### Linking this NPM repository
1) Go to this repository locally
2) Run `npm link`
3) Go to the folder housing your chromeless scripts
4) Run `npm link chromeless`
Now your local chromeless scripts will use your local development of chromeless.
## FAQ
### How is this different from [NightmareJS](https://github.com/segmentio/nightmare), PhantomJS or Selenium?
The `Chromeless` API is very similar to NightmareJS as their API is pretty awesome. The big difference is that `Chromeless` is based on Chrome in [headless-mode](https://developers.google.com/web/updates/2017/04/headless-chrome), and runs in a serverless function in AWS Lambda. The advantage of this is that you can run hundreds of browsers in parallel, without having to think about parallelisation. Running integration Tests for example is much faster.
### I'm new to AWS Lambda, is this still for me?
You still can use this locally without Lambda, so yes. Besides that, here is a [simple guide](https://github.com/prismagraphql/chromeless/tree/master/serverless) on how to set the lambda function up for `Chromeless`.
### How much does it cost to run Chromeless in production?
> The compute price is $0.00001667 per GB-s and the free tier provides 400,000 GB-s. The request price is $0.20 per 1 million requests and the free tier provides 1M requests per month.
This means you can easily execute > 100.000 tests for free in the free tier.
### Are there any limitations?
If you're running Chromeless on AWS Lambda, the execution cannot take longer than 5 minutes which is the current limit of Lambda. Besides that, every feature that's supported in Chrome is also working with Chromeless. The maximal number of concurrent function executions is 1000. [AWS API Limits](http://docs.aws.amazon.com/lambda/latest/dg/limits.html)
### Are there commercial options?
Although Chromeless is the easiest way to get started running Chrome on Lambda, you may not have time to build and manage your own visual testing toolkit. Commercial options include:
* [Chromatic](http://chromaticqa.com): Visual snapshot regression testing for [Storybook](https://storybook.js.org/).
## Troubleshooting
### Error: Unable to get presigned websocket URL and connect to it.
In case you get an error like this when running the Chromeless client:
```
{ HTTPError: Response code 403 (Forbidden)
at stream.catch.then.data (/code/chromeless/node_modules/got/index.js:182:13)
at process._tickDomainCallback (internal/process/next_tick.js:129:7)
name: 'HTTPError',
...
Error: Unable to get presigned websocket URL and connect to it.
```
Make sure that you're running at least version `1.19.0` of [`serverless`](https://github.com/serverless/serverless). It is a known [issue](https://github.com/serverless/serverless/issues/2450), that the API Gateway API keys are not setup correctly in older Serverless versions. Best is to run `npm run deploy` within the project as this will use the local installed version of `serverless`.
### Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev
In case the deployment of the serverless function returns an error like this:
```
Serverless Error ---------------------------------------
Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev
```
Please check, that there is no stack with the name `chromeless-serverless-dev` existing yet, otherwise serverless can't correctly provision the bucket.
### No command gets executed
In order for the commands to be processed, make sure, that you call one of the commands `screenshot`, `evaluate`, `cookiesGetAll` or `end` at the end of your execution chain.
## Contributors
A big thank you to all contributors and supporters of this repository 💚
## Credits
* [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface): Chromeless uses this package as an interface to Chrome
* [serverless-chrome](https://github.com/adieuadieu/serverless-chrome): Compiled Chrome binary that runs on AWS Lambda (Azure and GCP soon, too.)
* [NightmareJS](https://github.com/segmentio/nightmare): We draw a lot of inspiration for the API from this great tool
## Help & Community [](https://slack.graph.cool)
Join our [Slack community](http://slack.graph.cool/) if you run into issues or have questions. We love talking to you!

================================================
FILE: docs/api.md
================================================
# API Documentation
Chromeless provides TypeScript typings.
### Chromeless constructor options
`new Chromeless(options: ChromelessOptions)`
- `debug: boolean` Show debug output — Default: `false`
- `remote: boolean` Use remote chrome process — Default: `false`
- `implicitWait: boolean` Wait for element to exist before executing commands — Default: `false`
- `waitTimeout: number` Time in ms to wait for element to appear — Default: `10000`
- `scrollBeforeClick: boolean` Scroll to element before clicking, usefull if element is outside of viewport — Default: `false`
- `viewport: any` Viewport dimensions — Default: `{width: 1440, height: 900, scale: 1}`
- `launchChrome: boolean` Auto-launch chrome (local) — Default: `true`
- `cdp: CDPOptions` Chome Debugging Protocol Options — Default: `{host: 'localhost', port: 9222, secure: false, closeTab: true}`
### Chromeless methods
- [`end()`](#api-end)
### Chrome methods
- [`goto(url: string, timeout?: number)`](#api-goto)
- [`setUserAgent(useragent: string)`](#api-setuseragent)
- [`click(selector: string, x?: number, y?: number)`](#api-click)
- [`wait(timeout: number)`](#api-wait-timeout)
- [`wait(selector: string, timeout?: number)`](#api-wait-selector)
- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet
- [`clearCache()`](#api-clearcache)
- [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage)
- [`focus(selector: string)`](#api-focus)
- [`press(keyCode: number, count?: number, modifiers?: any)`](#api-press)
- [`type(input: string, selector?: string)`](#api-type)
- [`back()`](#api-back) - Not implemented yet
- [`forward()`](#api-forward) - Not implemented yet
- [`refresh()`](#api-refresh) - Not implemented yet
- [`mousedown(selector: string)`](#api-mousedown)
- [`mouseup(selector: string)`](#api-mouseup)
- [`scrollTo(x: number, y: number)`](#api-scrollto)
- [`scrollToElement(selector: string)`](#api-scrolltoelement)
- [`setHtml(html: string)`](#api-sethtml)
- [`setViewport(options: DeviceMetrics)`](#api-setviewport)
- [`evaluate(fn: (...args: any[]) => void, ...args: any[])`](#api-evaluate)
- [`inputValue(selector: string)`](#api-inputvalue)
- [`exists(selector: string)`](#api-exists)
- [`screenshot(selector: string, options: ScreenshotOptions)`](#api-screenshot)
- [`pdf(options?: PdfOptions)`](#api-pdf)
- [`html()`](#api-html)
- [`cookies()`](#api-cookies)
- [`cookies(name: string)`](#api-cookies-name)
- [`cookies(query: CookieQuery)`](#api-cookies-query) - Not implemented yet
- [`allCookies()`](#api-all-cookies)
- [`setCookies(name: string, value: string)`](#api-setcookies)
- [`setCookies(cookie: Cookie)`](#api-setcookies-one)
- [`setCookies(cookies: Cookie[])`](#api-setcookies-many)
- [`deleteCookies(name: string)`](#api-deletecookies)
- [`clearCookies()`](#api-clearcookies)
---------------------------------------
### end(): Promise
End the Chromeless session. Locally this will disconnect from Chrome. Over the Proxy, this will end the session, terminating the Lambda function.
It returns the last value that has been evaluated.
```js
await chromeless.end()
```
---------------------------------------
### goto(url: string, timeout?: number): Chromeless
Navigate to a URL.
__Arguments__
- `url` - URL to navigate to
- `timeout` -How long to wait for page to load (default is value of waitTimeout option)
__Example__
```js
await chromeless.goto('https://google.com/')
```
---------------------------------------
### setUserAgent(useragent: string): Chromeless
Set the useragent of the browser. It should be called before `.goto()`.
__Arguments__
- `useragent` - UserAgent to use
__Example__
```js
await chromeless.setUserAgent('Custom Chromeless UserAgent x.x.x')
```
---------------------------------------
### click(selector: string, x?: number, y?: number): Chromeless
Click on something in the DOM.
__Arguments__
- `selector` - DOM selector for element to click
- `x` - Offset from the left of the element, default width/2
- `y` - Offset from the top of the element, default height/2
__Example__
```js
await chromeless.click('#button')
await chromeless.click('#button', 20, 100)
```
---------------------------------------
### wait(timeout: number): Chromeless
Wait for some duration. Useful for waiting for things download.
__Arguments__
- `timeout` - How long to wait, in ms
__Example__
```js
await chromeless.wait(1000)
```
---------------------------------------
### wait(selector: string, timeout?: number): Chromeless
Wait until something appears. Useful for waiting for things to render.
__Arguments__
- `selector` - DOM selector to wait for
- `timeout` - How long to wait for element to appear (default is value of waitTimeout option)
__Example__
```js
await chromeless.wait('div#loaded')
await chromeless.wait('div#loaded', 1000)
```
---------------------------------------
### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless
Not implemented yet
Wait until a function returns. You can also return some Promise that will be resolved at some point.
__Arguments__
- `fn` - Function to wait for
- `[arguments]` - Arguments to pass to the function
__Example__
```js
await chromeless.wait(() => {
return new Promise((resolve, reject) => {
// do something async, setTimeout...
resolve();
});
})
```
---------------------------------------
### clearCache(): Chromeless
Clears browser cache.
Service workers and Storage (IndexedDB, WebSQL, etc) needs to be cleared separately. More information at the [Chrome Devtools Protocol website](https://chromedevtools.github.io/devtools-protocol/tot).
__Example__
```js
await chromeless.clearCache()
```
---------------------------------------
### clearStorage(origin: string, storageTypes: string): Chromeless
Clears browser storage.
__Arguments__
- `origin` - Security origin for the storage type we wish to clear
- `storageTypes` - A string comma separated list of chrome storage types. Allowed values include: appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage, all, other. More information at the [Chrome Devtools Protocol website](https://chromedevtools.github.io/devtools-protocol/tot/Storage/).
__Example__
```js
await chromeless.clearStorage('http://localhost', 'local_storage, websql')
await chromeless.clearStorage('*', 'all')
```
---------------------------------------
### focus(selector: string): Chromeless
Provide focus on a DOM element.
__Arguments__
- `selector` - DOM selector to focus
__Example__
```js
await chromeless.focus('input#searchField')
```
---------------------------------------
### press(keyCode: number, count?: number, modifiers?: any): Chromeless
Send a key press. Enter, for example.
__Arguments__
- `keyCode` - Key code to send
- `count` - How many times to send the key press
- `modifiers` - Modifiers to send along with the press (e.g. control, command, or alt)
__Example__
```js
await chromeless.press(13)
```
---------------------------------------
### type(input: string, selector?: string): Chromeless
Type something (into a field, for example).
__Arguments__
- `input` - String to type
- `selector` - DOM element to type into
__Example__
```js
const result = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
```
---------------------------------------
### back() - Not implemented yet
Not implemented yet
---------------------------------------
### forward() - Not implemented yet
Not implemented yet
---------------------------------------
### refresh() - Not implemented yet
Not implemented yet
---------------------------------------
### mousedown(selector: string): Chromeless
Send mousedown event on something in the DOM.
__Arguments__
- `selector` - DOM selector for element to send mousedown event
__Example__
```js
await chromeless.mousedown('#item')
```
---------------------------------------
### mouseup(selector: string): Chromeless
Send mouseup event on something in the DOM.
__Arguments__
- `selector` - DOM selector for element to send mouseup event
__Example__
```js
await chromeless.mouseup('#placeholder')
```
---------------------------------------
### scrollTo(x: number, y: number): Chromeless
Scroll to somewhere in the document.
__Arguments__
- `x` - Offset from the left of the document
- `y` - Offset from the top of the document
__Example__
```js
await chromeless.scrollTo(0, 500)
```
---------------------------------------
### scrollToElement(selector: string): Chromeless
Scroll to location of element. Behavior is simiar to `` — target element will be at the top of viewport
__Arguments__
- `selector` - DOM selector for element to scroll to
__Example__
```js
await chromeless.scrollToElement('.button')
```
---------------------------------------
### setHtml(html: string): Chromeless
Sets given markup as the document's HTML.
__Arguments__
- `html` - HTML to set as the document's markup.
__Example__
```js
await chromeless.setHtml('Hello world!
')
```
---------------------------------------
### setExtraHTTPHeaders(headers: Headers): Chromeless
Sets extra HTTP headers.
__Arguments__
- `headers` - headers as keys / values of JSON object
__Example__
```js
await chromeless.setExtraHTTPHeaders({
'accept-language': 'en-US,en;q=0.8'
})
```
---------------------------------------
### setViewport(options:DeviceMetrics)
Resize the viewport. Useful if you want to capture more or less of the document in a screenshot.
__Arguments__
- `options` - DeviceMetrics object
__Example__
```js
await chromeless.setViewport({width: 1024, height: 600, scale: 1})
```
---------------------------------------
### evaluate(fn: (...args: any[]) => void, ...args: any[]): Chromeless
Evaluate Javascript code within Chrome in the context of the DOM. Returns the resulting value or a Promise.
__Arguments__
- `fn` - Function to evaluate within Chrome, can be async (Promise).
- `[arguments]` - Arguments to pass to the function
__Example__
```js
await chromeless.evaluate(() => {
// this will be executed in Chrome
const links = [].map.call(
document.querySelectorAll('.g h3 a'),
a => ({title: a.innerText, href: a.href})
)
return JSON.stringify(links)
})
```
---------------------------------------
### inputValue(selector: string): Chromeless
Get the value of an input field.
__Arguments__
- `selector` - DOM input element
__Example__
```js
await chromeless.inputValue('input#searchField')
```
---------------------------------------
### exists(selector: string): Chromeless
Test if a DOM element exists in the document.
__Arguments__
- `selector` - DOM element to check for
__Example__
```js
await chromeless.exists('div#ready')
```
---------------------------------------
### screenshot(selector: string, options: ScreenshotOptions): Chromeless
Take a screenshot of the document as framed by the viewport or of a specific element (by a selector).
When running Chromeless locally this returns the local file path to the screenshot image.
When run over the Chromeless Proxy service, a URL to the screenshot on S3 is returned.
__Arguments__
- `selector` - DOM element to take a screenshot of,
- `options` - An options object with the following props
- `options.filePath` - A file path override in case of working locally
- `options.omitBackground` - Boolean to remove default white background
__Examples__
```js
const screenshot = await chromeless
.goto('https://google.com/')
.screenshot()
console.log(screenshot) // prints local file path or S3 URL
```
```js
const screenshot = await chromeless
.goto('https://google.com/')
.screenshot('#hplogo', { filePath: path.join(__dirname, 'google-logo.png') })
console.log(screenshot) // prints local file path or S3 URL
```
```js
const screenshot = await chromeless
.goto('https://google.com/')
.screenshot({ filePath: path.join(__dirname, 'google-search.png') })
console.log(screenshot) // prints local file path or S3 URL
```
---------------------------------------
### pdf(options?: PdfOptions) - Chromeless
Print to a PDF of the document as framed by the viewport.
When running Chromeless locally this returns the local file path to the PDF.
When run over the Chromeless Proxy service, a URL to the PDF on S3 is returned.
Requires that Chrome be running headless-ly. [More](https://github.com/graphcool/chromeless/issues/146)
__Arguments__
- `options` - An object containing overrides for [printToPDF() parameters](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF)
__Example__
```js
const pdf = await chromeless
.goto('https://google.com/')
.pdf({landscape: true})
console.log(pdf) // prints local file path or S3 URL
```
---------------------------------------
### html(): Chromeless
Get full HTML of the loaded page.
__Example__
```js
const html = await chromeless
.setHtml('Hello world!
')
.html()
console.log(html) // Hello world!
```
---------------------------------------
### cookies(): Chromeless
Returns all browser cookies for the current URL.
__Example__
```js
await chromeless.cookies()
```
---------------------------------------
### cookies(name: string): Chromeless
Returns a specific browser cookie by name for the current URL.
__Arguments__
- `name` - Name of the cookie to get
__Example__
```js
const cookie = await chromeless.cookies('creepyTrackingCookie')
```
---------------------------------------
### cookies(query: CookieQuery) - Not implemented yet
Not implemented yet
---------------------------------------
### allCookies(): Chromeless
Returns all browser cookies. Nam nom nom.
__Example__
```js
await chromeless.allCookies()
```
---------------------------------------
### setCookies(name: string, value: string): Chromeless
Sets a cookie with the given name and value.
__Arguments__
- `name` - Name of the cookie
- `value` - Value of the cookie
__Example__
```js
await chromeless.setCookies('visited', '1')
```
---------------------------------------
### setCookies(cookie: Cookie): Chromeless
Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.
__Arguments__
- `cookie` - The cookie data to set
__Example__
```js
await chromeless.setCookies({
url: 'http://google.com/',
domain: 'google.com',
name: 'userData',
value: '{}',
path: '/',
expires: 0,
size: 0,
httpOnly: false,
secure: true,
session: true,
})
```
---------------------------------------
### setCookies(cookies: Cookie[]): Chromeless
Sets many cookies with the given cookie data; may overwrite equivalent cookies if they exist.
__Arguments__
- `url` - URL to navigate to
__Example__
```js
await chromeless.setCookies([
{
url: 'http://google.com/',
domain: 'google.com',
name: 'userData',
value: '{}',
path: '/',
expires: 0,
size: 0,
httpOnly: false,
secure: true,
session: true,
}, {
url: 'http://bing.com/',
domain: 'bing.com',
name: 'userData',
value: '{}',
path: '/',
expires: 0,
size: 0,
httpOnly: false,
secure: true,
session: true,
}
])
```
---------------------------------------
### deleteCookies(name: string) - Not implemented yet
Delete a specific cookie.
__Arguments__
- `name` - name of the cookie
__Example__
```js
await chromeless.deleteCookies('cookieName')
```
---------------------------------------
### clearCookies(): Chromeless
Clears all browser cookies.
__Example__
```js
await chromeless.clearCookies()
```
---------------------------------------
### clearInput(selector: string): Chromeless
Clear input text.
__Example__
```js
await chromeless.clearInput('#username')
```
---------------------------------------
### setFileInput(selector: string, files: string | string[]): Chromeless
Set file(s) for selected file input.
Currently not supported in the Proxy. Progress tracked in [#186](https://github.com/graphcool/chromeless/issues/186)
__Example__
```js
await chromeless.setFileInput('.uploader', '/User/Me/Documents/img.jpg')
```
================================================
FILE: examples/extract-google-results.js
================================================
const { Chromeless } = require('chromeless')
async function run() {
const chromeless = new Chromeless({ remote: true })
const links = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
.press(13)
.wait('#resultStats')
.evaluate(() => {
// this will be executed in headless chrome
const links = [].map.call(
document.querySelectorAll('.g h3 a'),
a => ({title: a.innerText, href: a.href})
)
return JSON.stringify(links)
})
// you can still use the method chaining API after evaluating
// when you're done, at any time you can call `.then` (in our case `await`)
.scrollTo(0, 1000)
console.log(links)
await chromeless.end()
}
run().catch(console.error.bind(console))
================================================
FILE: examples/google-pdf.js
================================================
const { Chromeless } = require('chromeless')
async function run() {
const chromeless = new Chromeless()
const pdf = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
.press(13)
.wait('#resultStats')
.pdf({ displayHeaderFooter: true, landscape: true })
console.log(pdf) // prints local file path or S3 url
await chromeless.end()
}
run().catch(console.error.bind(console))
================================================
FILE: examples/google-screenshot.js
================================================
const { Chromeless } = require('chromeless')
async function run() {
const chromeless = new Chromeless()
const screenshot = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
.press(13)
.wait('#resultStats')
.screenshot()
console.log(screenshot) // prints local file path or S3 url
await chromeless.end()
}
run().catch(console.error.bind(console))
================================================
FILE: examples/mocha-chai-test-example.js
================================================
const { Chromeless } = require('chromeless')
const { expect } = require('chai')
// make sure you do npm i chai
// to run this example just run
// mocha path/to/this/file
describe('When searching on google', function () {
it('shows results', async function () {
this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error
const chromeless = new Chromeless()
await chromeless.goto('https://google.com')
.wait('input[name="q"]')
.type('chromeless github', 'input[name="q"]')
.press(13) // press enter
.wait('#resultStats')
const result = await chromeless.exists('a[href*="graphcool/chromeless"]')
expect(result).to.be.true
await chromeless.end()
})
})
describe('When clicking on the image of the demo playground', function () {
it('should redirect to the demo', async function () {
this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error
const chromeless = new Chromeless()
await chromeless.goto('https://github.com/graphcool/chromeless')
.wait('a[href="https://chromeless.netlify.com/"]')
.click('a[href="https://chromeless.netlify.com/"]')
.wait('#root')
const url = await chromeless.evaluate(url => window.location.href)
expect(url).to.match(/^https\:\/\/chromeless\.netlify\.com/)
await chromeless.end()
})
})
================================================
FILE: examples/mouse-event-example.js
================================================
const { Chromeless } = require('chromeless')
async function run() {
const chromeless = new Chromeless()
const screenshot = await chromeless
.goto('https://www.google.com')
.mousedown('input[name="btnI"]')
.mouseup('input[name="btnI"]')
.wait('.latest-doodle')
.screenshot()
console.log(screenshot)
await chromeless.end()
}
run().catch(console.error.bind(console))
================================================
FILE: examples/twitter.js
================================================
const { Chromeless } = require('chromeless')
const twitterUsername = "xxx"
const twitterPassword = "xxx"
async function run() {
const chromeless = new Chromeless()
const screenshot = await chromeless
.goto('https://twitter.com/login/')
.type(twitterUsername, '.js-username-field')
.type(twitterPassword, '.js-password-field')
.click('button[type="submit"]')
.wait('.status')
.screenshot()
await chromeless.end()
}
run().catch(console.error.bind(console))
================================================
FILE: package.json
================================================
{
"name": "chromeless",
"version": "1.4.0",
"description": "🖥 Chrome automation made simple. Runs locally or headless on AWS Lambda.",
"homepage": "https://github.com/graphcool/chromeless",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/graphcool/chromeless.git"
},
"bug": {
"url": "https://github.com/graphcool/chromeless/issues"
},
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">= 6.10.0"
},
"scripts": {
"ava": "tsc -d && nyc ava",
"build": "npm run clean && tsc -d",
"clean": "rimraf dist",
"coverage": "npm run ava",
"precommit": "lint-staged",
"commitmsg": "commitlint -e $GIT_PARAMS",
"prettier": "prettier --list-different --write \"**/*.{ts,json}\"",
"test": "npm run lint && npm run ava",
"lint": "npm run prettier && npm run tslint",
"tslint": "tslint -c tslint.json -p tsconfig.json --exclude 'node_modules/**'",
"watch": "tsc -w",
"watch:test": "tsc -d -w & ava --watch",
"semantic-release": "semantic-release"
},
"dependencies": {
"aws-sdk": "^2.177.0",
"bluebird": "^3.5.1",
"chrome-launcher": "^0.10.0",
"chrome-remote-interface": "^0.25.5",
"cuid": "^2.1.0",
"form-data": "^2.3.1",
"got": "^8.0.0",
"mqtt": "^2.15.0"
},
"devDependencies": {
"@commitlint/config-conventional": "^7.0.0",
"@types/bluebird": "^3.5.19",
"@types/cuid": "^1.3.0",
"@types/node": "^10.0.3",
"ava": "^0.25.0",
"commitlint": "^7.0.0",
"husky": "^0.14.3",
"lint-staged": "^7.0.0",
"nyc": "^12.0.2",
"prettier": "1.11.1",
"rimraf": "^2.6.2",
"semantic-release": "^15.0.2",
"tslint": "^5.8.0",
"typescript": "^2.6.2"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"lint-staged": {
"*.{ts}": [
"prettier --parser typescript --no-semi --single-quote --trailing-comma all --write",
"tslint",
"git add"
],
"*.{js}": [
"prettier --no-semi --single-quote --trailing-comma all --write",
"lint",
"git add"
]
}
}
================================================
FILE: serverless/README.md
================================================
# Chromeless Proxy service
A [Serverless](https://serverless.com/) AWS Lambda service for running and interacting with Chrome remotely with Chromeless.
## Contents
1. [Setup](#setup)
1. [Using the Proxy](#using-the-proxy)
## Setup
Clone this repository and enter the `serverless` directory:
```bash
git clone https://github.com/graphcool/chromeless.git
cd chromeless/serverless
npm install
```
### Configure
Next, modify the `custom` section in `serverless.yml`.
You must set `awsIotHost` to the your AWS IoT Custom Endpoint for your AWS region. You can find this with the AWS CLI with `aws iot describe-endpoint --output text` or by navigating to the AWS IoT Console and going to Settings.
For example:
```yaml
...
custom:
stage: dev
debug: "*" # false if you don't want noise in CloudWatch
awsIotHost: ${env:AWS_IOT_HOST}
...
```
You may also need to change the region in the `provider` section in `serverless.yml`:
```yaml
...
provider:
name: aws
runtime: nodejs6.10
stage: ${self:custom.stage}
region: YOUR_REGION_HERE
...
```
**Note:** The AWS Lambda function, API Gateway and IoT must all be in the _same_ region.
**Note:** Deploying from Windows is currently not supported. See [#70](https://github.com/graphcool/chromeless/issues/70#issuecomment-318634457)
### Credentials
Before you can deploy, you must configure your AWS credentials either by defining `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environmental variables, or using an AWS profile. You can read more about this on the [Serverless Credentials Guide](https://serverless.com/framework/docs/providers/aws/guide/credentials/).
In short, either:
```bash
export AWS_PROFILE=
```
or
```bash
export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=
```
### Deploy
Once configured, deploying the service can be done with:
```bash
npm run deploy
```
Once completed, some service information will be logged. Make note of the `session` GET endpoint and the value of the `dev-chromeless-session-key` API key. You'll need them when using Chromeless through the Proxy.
```log
Service Information
service: chromeless-serverless
stage: dev
region: eu-west-1
api keys:
dev-chromeless-session-key: X-your-api-key-here-X
endpoints:
GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/version
OPTIONS - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/
GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/
functions:
run: chromeless-serverless-dev-run
version: chromeless-serverless-dev-version
session: chromeless-serverless-dev-session
disconnect: chromeless-serverless-dev-disconnect
```
## Using the Proxy
Connect to the proxy service with the `remote` option parameter on the Chromeless constructor. You must provide the endpoint URL provided during deployment either as an argument or set it in the `CHROMELESS_ENDPOINT_URL` environment variable. Note that this endpoint is _different_ from the AWS IoT Custom Endpoint. The Proxy's endpoint URL you want to use will look something like `https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/`
### Option 1: Environment Variables
```bash
export CHROMELESS_ENDPOINT_URL=https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev
export CHROMELESS_ENDPOINT_API_KEY=your-api-key-here
```
and
```js
const chromeless = new Chromeless({
remote: true,
})
```
### Option 2: Constructor options
```js
const chromeless = new Chromeless({
remote: {
endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',
apiKey: 'your-api-key-here',
},
})
```
### Full Example
```js
const Chromeless = require('chromeless').default
async function run() {
const chromeless = new Chromeless({
remote: {
endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',
apiKey: 'your-api-key-here'
},
})
const screenshot = await chromeless
.goto('https://www.google.com')
.type('chromeless', 'input[name="q"]')
.press(13)
.wait('#resultStats')
.screenshot()
console.log(screenshot) // prints local file path or S3 url
await chromeless.end()
}
run().catch(console.error.bind(console))
```
================================================
FILE: serverless/package.json
================================================
{
"name": "chromeless-remotechrome-service",
"version": "1.3.0",
"description": "The Chromeless Proxy AWS Lambda service",
"homepage": "https://github.com/graphcool/chromeless",
"license": "MIT",
"engines": {
"node": ">= 6.10.0"
},
"scripts": {
"deploy": "serverless deploy"
},
"dependencies": {
"aws4": "^1.6.0",
"chromeless": "^1.3.0",
"cuid": "^1.3.8",
"mqtt": "^2.11.0",
"source-map-support": "^0.4.15"
},
"devDependencies": {
"@types/cuid": "^1.3.0",
"@types/node": "^8.0.15",
"serverless": "^1.19.0",
"serverless-offline": "^3.15.3",
"serverless-plugin-chrome": "^1.0.0-11",
"serverless-plugin-typescript": "^1.0.0"
}
}
================================================
FILE: serverless/serverless.yml
================================================
service: chromeless-serverless
custom:
stage: dev
debug: "*"
awsIotHost: ${env:AWS_IOT_HOST}
chrome:
functions:
- run
provider:
name: aws
runtime: nodejs6.10
stage: ${self:custom.stage}
region: eu-west-1
environment:
DEBUG: ${self:custom.debug}
AWS_IOT_HOST: ${self:custom.awsIotHost}
apiKeys:
- ${self:custom.stage}-chromeless-session-key
iamRoleStatements:
- Effect: "Allow"
Action:
- "iot:Connect"
- "iot:Publish"
- "iot:Subscribe"
- "iot:Receive"
- "iot:GetThingShadow"
- "iot:UpdateThingShadow"
Resource: "*"
- Effect: "Allow"
Action:
- s3:*
Resource:
Fn::Join:
- ""
- - "arn:aws:s3:::"
- Ref: AWS::AccountId
- "-"
- Ref: AWS::Region
- -chromeless
- /*
plugins:
- serverless-plugin-typescript
- serverless-plugin-chrome
- serverless-offline
functions:
run:
memorySize: 1536
timeout: 300
handler: src/run.default
events:
- iot:
sql: "SELECT * FROM 'chrome/new-session'"
environment:
CHROMELESS_S3_BUCKET_NAME:
Fn::Join:
- ""
- - Ref: AWS::AccountId
- "-"
- Ref: AWS::Region
- -chromeless
CHROMELESS_S3_OBJECT_KEY_PREFIX: ""
CHROMELESS_S3_OBJECT_ACL: "public-read"
CHROMELESS_S3_BUCKET_URL:
Fn::GetAtt:
- Bucket
- DomainName
version:
memorySize: 128
handler: src/version.default
events:
- http:
path: /version
method: GET
session:
memorySize: 128
timeout: 10
handler: src/session.default
events:
- http:
method: OPTIONS
path: /
private: true
- http:
method: GET
path: /
private: true
disconnect:
memorySize: 256
handler: src/disconnect.default
timeout: 10
events:
- iot:
sql: "SELECT * FROM 'chrome/last-will'"
resources:
Resources:
RunLogGroup:
Properties:
RetentionInDays: 7
VersionLogGroup:
Properties:
RetentionInDays: 7
SessionLogGroup:
Properties:
RetentionInDays: 7
DisconnectLogGroup:
Properties:
RetentionInDays: 7
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
Fn::Join:
- ""
- - Ref: AWS::AccountId
- "-"
- Ref: AWS::Region
- -chromeless
LifecycleConfiguration:
Rules:
- ExpirationInDays: 1
Status: Enabled
================================================
FILE: serverless/src/disconnect.ts
================================================
import * as AWS from 'aws-sdk'
import { debug } from './utils'
const iotData = new AWS.IotData({ endpoint: process.env.AWS_IOT_HOST })
export default async ({ channelId }, context, callback): Promise => {
debug('Disconnect on', channelId)
let params = {
topic: `chrome/${channelId}/end`,
payload: JSON.stringify({ channelId, client: true, disconnected: true }),
qos: 1,
}
iotData.publish(params, callback)
}
================================================
FILE: serverless/src/run.ts
================================================
import 'source-map-support/register'
import { LocalChrome, Queue, ChromelessOptions } from 'chromeless'
import { connect as mqtt, MqttClient } from 'mqtt'
import { createPresignedURL, debug } from './utils'
export default async (
{ channelId, options },
context,
callback,
chromeInstance
): Promise => {
// used to block requests from being processed while we're exiting
let endingInvocation = false
let timeout
let executionCheckInterval
debug('Invoked with data: ', channelId, options)
const chrome = new LocalChrome({
...options,
remote: false,
launchChrome: false,
cdp: { closeTab: true },
})
const queue = new Queue(chrome)
const TOPIC_CONNECTED = `chrome/${channelId}/connected`
const TOPIC_REQUEST = `chrome/${channelId}/request`
const TOPIC_RESPONSE = `chrome/${channelId}/response`
const TOPIC_END = `chrome/${channelId}/end`
const channel = mqtt(createPresignedURL())
if (process.env.DEBUG) {
channel.on('error', error => debug('WebSocket error', error))
channel.on('offline', () => debug('WebSocket offline'))
}
/*
Clean up function whenever we want to end the invocation.
Importantly we publish a message that we're disconnecting, and then
we kill the running Chrome instance.
*/
const end = (topic_end_data = {}) => {
if (!endingInvocation) {
endingInvocation = true
clearInterval(executionCheckInterval)
clearTimeout(timeout)
channel.unsubscribe(TOPIC_END, () => {
channel.publish(TOPIC_END, JSON.stringify({ channelId, chrome: true, ...topic_end_data }), {
qos: 0,
}, async () => {
channel.end()
await chrome.close()
await chromeInstance.kill()
callback()
})
})
}
}
const newTimeout = () =>
setTimeout(async () => {
debug('Timing out. No requests received for 30 seconds.')
await end({ inactivity: true })
}, 30000)
/*
When we're almost out of time, we clean up.
Importantly this makes sure that Chrome isn't running on the next invocation
and publishes a message to the client letting it know we're disconnecting.
*/
executionCheckInterval = setInterval(async () => {
if (context.getRemainingTimeInMillis() < 5000) {
debug('Ran out of execution time.')
await end({ outOfTime: true })
}
}, 1000)
channel.on('connect', () => {
debug('Connected to AWS IoT broker')
/*
Publish that we've connected. This lets the client know that
it can start sending requests (commands) for us to process.
*/
channel.publish(TOPIC_CONNECTED, JSON.stringify({}), { qos: 1 })
/*
The main bit. Listen for requests from the client, handle them
and respond with the result.
*/
channel.subscribe(TOPIC_REQUEST, () => {
debug(`Subscribed to ${TOPIC_REQUEST}`)
timeout = newTimeout()
channel.on('message', async (topic, buffer) => {
if (TOPIC_REQUEST === topic && !endingInvocation) {
const message = buffer.toString()
clearTimeout(timeout)
debug(`Message from ${TOPIC_REQUEST}`, message)
const command = JSON.parse(message)
try {
const result = await queue.process(command)
const remoteResult = JSON.stringify({
value: result,
})
debug('Chrome result', result)
channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 })
} catch (error) {
const remoteResult = JSON.stringify({
error: error.toString(),
})
debug('Chrome error', error)
channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 })
}
timeout = newTimeout()
}
})
})
/*
Handle diconnection from the client.
Either the client purposfully ended the session, or the client
connection was abruptly ended resulting in a last-will message
being dispatched by the IoT MQTT broker.
*/
channel.subscribe(TOPIC_END, async () => {
channel.on('message', async (topic, buffer) => {
if (TOPIC_END === topic) {
const message = buffer.toString()
const data = JSON.parse(message)
debug(`Message from ${TOPIC_END}`, message)
debug(
`Client ${data.disconnected ? 'disconnected' : 'ended session'}.`
)
await end()
debug('Ended successfully.')
}
})
})
})
}
================================================
FILE: serverless/src/session.ts
================================================
import { connect as mqtt, MqttClient } from 'mqtt'
import * as cuid from 'cuid'
import { createPresignedURL, debug } from './utils'
export default async (event, context, callback): Promise => {
const url = createPresignedURL()
const channelId = cuid()
callback(null, {
statusCode: 200,
body: JSON.stringify({ url, channelId }),
})
}
================================================
FILE: serverless/src/utils.ts
================================================
import * as aws4 from 'aws4'
/*
This creates a presigned URL for accessing the AWS IoT MQTT Broker.
Notably, the sessionToken is simply tacked on to the end, and not signed.
Because AWS. Thank you @shortjared for your help pointing this out.
*/
export function createPresignedURL(
{
host = process.env.AWS_IOT_HOST,
path = '/mqtt',
region = process.env.AWS_REGION,
service = 'iotdevicegateway',
accessKeyId = process.env.AWS_ACCESS_KEY_ID,
secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY,
sessionToken = process.env.AWS_SESSION_TOKEN,
// expires = 0, // @TODO: 300, check if this is working http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
} = {},
): string {
const signed = aws4.sign(
{
host,
path,
service,
region,
signQuery: true,
// headers: {
// 'X-Amz-Expires': expires,
// },
},
{
accessKeyId,
secretAccessKey,
},
)
return `wss://${host}${signed.path}&X-Amz-Security-Token=${encodeURIComponent(
sessionToken,
)}`
}
export function debug(...log) {
if (process.env.DEBUG) {
console.log(
...log.map(
argument =>
typeof argument === 'object'
? JSON.stringify(argument, null, 2)
: argument
)
)
}
}
================================================
FILE: serverless/src/version.ts
================================================
import { version as chromelessVersion } from 'chromeless'
const serverlessChromelessVersion = require('../package.json').version
export default async (event, context, callback): Promise => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
chromeless: chromelessVersion,
serverlessChromeless: serverlessChromelessVersion,
}),
})
}
================================================
FILE: serverless/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"rootDir": ".",
"target": "es6",
"sourceMap": true,
"moduleResolution": "node"
},
"exclude": [
"node_modules"
]
}
================================================
FILE: src/__tests__/test.html
================================================
Title
This is a test page for Chromeless unit tests
================================================
FILE: src/api.ts
================================================
import ChromeLocal from './chrome/local'
import ChromeRemote from './chrome/remote'
import Queue from './queue'
import {
ChromelessOptions,
Headers,
Cookie,
CookieQuery,
PdfOptions,
DeviceMetrics,
ScreenshotOptions,
} from './types'
import { getDebugOption } from './util'
import { isArray } from 'util'
export default class Chromeless implements Promise {
private queue: Queue
private lastReturnPromise: Promise
constructor(options: ChromelessOptions = {}, copyInstance?: Chromeless) {
if (copyInstance) {
this.queue = copyInstance.queue
this.lastReturnPromise = copyInstance.lastReturnPromise
return
}
const mergedOptions: ChromelessOptions = {
debug: getDebugOption(),
waitTimeout: 10000,
remote: false,
implicitWait: true,
scrollBeforeClick: false,
launchChrome: true,
...options,
viewport: {
scale: 1,
...options.viewport,
},
cdp: {
host: process.env['CHROMELESS_CHROME_HOST'] || 'localhost',
port: parseInt(process.env['CHROMELESS_CHROME_PORT'], 10) || 9222,
secure: false,
closeTab: true,
...options.cdp,
},
}
const chrome = mergedOptions.remote
? new ChromeRemote(mergedOptions)
: new ChromeLocal(mergedOptions)
this.queue = new Queue(chrome)
this.lastReturnPromise = Promise.resolve(undefined)
}
/*
* The following 3 members are needed to implement a Promise
*/
readonly [Symbol.toStringTag]: 'Promise'
then(
onFulfill?: ((value: T) => U | PromiseLike) | null,
onReject?: ((error: any) => U | PromiseLike) | null,
): Promise {
return this.lastReturnPromise.then(onFulfill, onReject) as Promise
}
catch(onrejected?: (reason: any) => U | PromiseLike): Promise {
return this.lastReturnPromise.catch(onrejected) as Promise
}
goto(url: string, timeout?: number): Chromeless {
this.queue.enqueue({ type: 'goto', url, timeout })
return this
}
setUserAgent(useragent: string): Chromeless {
this.queue.enqueue({ type: 'setUserAgent', useragent })
return this
}
click(selector: string, x?: number, y?: number): Chromeless {
this.queue.enqueue({ type: 'click', selector, x, y })
return this
}
wait(timeout: number): Chromeless
wait(selector: string, timeout?: number): Chromeless
wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless
wait(firstArg, ...args: any[]): Chromeless {
switch (typeof firstArg) {
case 'number': {
this.queue.enqueue({ type: 'wait', timeout: firstArg })
break
}
case 'string': {
this.queue.enqueue({
type: 'wait',
selector: firstArg,
timeout: args[0],
})
break
}
case 'function': {
this.queue.enqueue({ type: 'wait', fn: firstArg, args })
break
}
default:
throw new Error(`Invalid wait arguments: ${firstArg} ${args}`)
}
return this
}
clearCache(): Chromeless {
this.queue.enqueue({ type: 'clearCache' })
return this
}
clearStorage(origin: string, storageTypes: string): Chromeless {
this.queue.enqueue({ type: 'clearStorage', origin, storageTypes })
return this
}
focus(selector: string): Chromeless {
this.queue.enqueue({ type: 'focus', selector })
return this
}
press(keyCode: number, count?: number, modifiers?: any): Chromeless {
this.queue.enqueue({ type: 'press', keyCode, count, modifiers })
return this
}
type(input: string, selector?: string): Chromeless {
this.queue.enqueue({ type: 'type', input, selector })
return this
}
back(): Chromeless {
throw new Error('Not implemented yet')
}
forward(): Chromeless {
throw new Error('Not implemented yet')
}
refresh(): Chromeless {
throw new Error('Not implemented yet')
}
mousedown(selector: string): Chromeless {
this.queue.enqueue({ type: 'mousedown', selector })
return this
}
mouseup(selector: string): Chromeless {
this.queue.enqueue({ type: 'mouseup', selector })
return this
}
mouseover(): Chromeless {
throw new Error('Not implemented yet')
}
scrollTo(x: number, y: number): Chromeless {
this.queue.enqueue({ type: 'scrollTo', x, y })
return this
}
scrollToElement(selector: string): Chromeless {
this.queue.enqueue({ type: 'scrollToElement', selector })
return this
}
setViewport(options: DeviceMetrics): Chromeless {
this.queue.enqueue({ type: 'setViewport', options })
return this
}
setHtml(html: string): Chromeless {
this.queue.enqueue({ type: 'setHtml', html })
return this
}
setExtraHTTPHeaders(headers: Headers): Chromeless {
this.queue.enqueue({ type: 'setExtraHTTPHeaders', headers })
return this
}
evaluate(
fn: (...args: any[]) => U,
...args: any[]
): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'returnCode',
fn: fn.toString(),
args,
})
return new Chromeless({}, this)
}
inputValue(selector: string): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'returnInputValue',
selector,
})
return new Chromeless({}, this)
}
exists(selector: string): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'returnExists',
selector,
})
return new Chromeless({}, this)
}
screenshot(
selector?: string,
options?: ScreenshotOptions,
): Chromeless {
if (typeof selector === 'object') {
options = selector
selector = undefined
}
this.lastReturnPromise = this.queue.process({
type: 'returnScreenshot',
selector,
options,
})
return new Chromeless({}, this)
}
html(): Chromeless {
this.lastReturnPromise = this.queue.process({ type: 'returnHtml' })
return new Chromeless({}, this)
}
htmlUrl(): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'returnHtmlUrl',
})
return new Chromeless({}, this)
}
pdf(options?: PdfOptions): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'returnPdf',
options,
})
return new Chromeless({}, this)
}
/**
* Get the cookies for the current url
*/
cookies(): Chromeless
/**
* Get a specific cookie for the current url
* @param name
*/
cookies(name: string): Chromeless
/**
* Get a specific cookie by query. Not implemented yet
* @param query
*/
cookies(query: CookieQuery): Chromeless
cookies(
nameOrQuery?: string | CookieQuery,
): Chromeless {
if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery !== 'string') {
throw new Error('Querying cookies is not implemented yet')
}
this.lastReturnPromise = this.queue.process({
type: 'cookies',
nameOrQuery,
})
return new Chromeless({}, this)
}
allCookies(): Chromeless {
this.lastReturnPromise = this.queue.process({
type: 'allCookies',
})
return new Chromeless({}, this)
}
setCookies(name: string, value: string): Chromeless
setCookies(cookie: Cookie): Chromeless
setCookies(cookies: Cookie[]): Chromeless
setCookies(nameOrCookies, value?: string): Chromeless {
this.queue.enqueue({ type: 'setCookies', nameOrCookies, value })
return this
}
deleteCookies(name: string, url: string): Chromeless {
if (typeof name === 'undefined') {
throw new Error('Cookie name should be defined.')
}
if (typeof url === 'undefined') {
throw new Error('Cookie url should be defined.')
}
this.queue.enqueue({ type: 'deleteCookies', name, url })
return this
}
clearCookies(): Chromeless {
this.queue.enqueue({ type: 'clearCookies' })
return this
}
clearInput(selector: string): Chromeless {
this.queue.enqueue({ type: 'clearInput', selector })
return this
}
setFileInput(selector: string, files: string): Chromeless
setFileInput(selector: string, files: string[]): Chromeless
setFileInput(selector: string, files: string | string[]): Chromeless {
if (!isArray(files)) {
files = [files]
}
this.queue.enqueue({ type: 'setFileInput', selector, files })
return this
}
async end(): Promise {
const result = await this.lastReturnPromise
await this.queue.end()
return result
}
}
================================================
FILE: src/chrome/local-runtime.ts
================================================
import {
Client,
Command,
ChromelessOptions,
Headers,
Cookie,
CookieQuery,
PdfOptions,
ScreenshotOptions,
} from '../types'
import {
nodeExists,
wait,
waitForNode,
click,
evaluate,
screenshot,
html,
htmlUrl,
pdf,
type,
getValue,
scrollTo,
scrollToElement,
setHtml,
setExtraHTTPHeaders,
press,
setViewport,
clearCookies,
deleteCookie,
getCookies,
setCookies,
getAllCookies,
version,
mousedown,
mouseup,
focus,
clearInput,
setFileInput,
writeToFile,
isS3Configured,
uploadToS3,
eventToPromise,
waitForPromise,
} from '../util'
export default class LocalRuntime {
private client: Client
private chromelessOptions: ChromelessOptions
private userAgentValue: string
constructor(client: Client, chromelessOptions: ChromelessOptions) {
this.client = client
this.chromelessOptions = chromelessOptions
}
async run(command: Command): Promise {
switch (command.type) {
case 'goto':
return this.goto(command.url, command.timeout)
case 'setViewport':
return setViewport(this.client, command.options)
case 'wait': {
if (command.selector) {
return this.waitSelector(command.selector, command.timeout)
} else if (command.timeout) {
return this.waitTimeout(command.timeout)
} else {
throw new Error('waitFn not yet implemented')
}
}
case 'clearCache':
return this.clearCache()
case 'clearStorage':
return this.clearStorage(command.origin, command.storageTypes)
case 'setUserAgent':
return this.setUserAgent(command.useragent)
case 'click':
return this.click(command.selector, command.x, command.y)
case 'returnCode':
return this.returnCode(command.fn, ...command.args)
case 'returnExists':
return this.returnExists(command.selector)
case 'returnScreenshot':
return this.returnScreenshot(command.selector, command.options)
case 'returnHtml':
return this.returnHtml()
case 'returnHtmlUrl':
return this.returnHtmlUrl()
case 'returnPdf':
return this.returnPdf(command.options)
case 'returnInputValue':
return this.returnInputValue(command.selector)
case 'type':
return this.type(command.input, command.selector)
case 'press':
return this.press(command.keyCode, command.count, command.modifiers)
case 'scrollTo':
return this.scrollTo(command.x, command.y)
case 'scrollToElement':
return this.scrollToElement(command.selector)
case 'deleteCookies':
return this.deleteCookies(command.name, command.url)
case 'clearCookies':
return this.clearCookies()
case 'setHtml':
return this.setHtml(command.html)
case 'setExtraHTTPHeaders':
return this.setExtraHTTPHeaders(command.headers)
case 'cookies':
return this.cookies(command.nameOrQuery)
case 'allCookies':
return this.allCookies()
case 'setCookies':
return this.setCookies(command.nameOrCookies, command.value)
case 'mousedown':
return this.mousedown(command.selector)
case 'mouseup':
return this.mouseup(command.selector)
case 'focus':
return this.focus(command.selector)
case 'clearInput':
return this.clearInput(command.selector)
case 'setFileInput':
return this.setFileInput(command.selector, command.files)
default:
throw new Error(`No such command: ${JSON.stringify(command)}`)
}
}
private async goto(
url: string,
waitTimeout: number = this.chromelessOptions.waitTimeout,
): Promise {
const { Network, Page } = this.client
await Promise.all([Network.enable(), Page.enable()])
if (!this.userAgentValue) this.userAgentValue = `Chromeless ${version}`
await Network.setUserAgentOverride({ userAgent: this.userAgentValue })
const e2p = eventToPromise()
Page.loadEventFired(e2p.onEvent)
await Page.navigate({ url })
await waitForPromise(e2p.fired(), waitTimeout, 'page load event')
this.log(`Navigated to ${url}`)
}
private async clearCache(): Promise {
const { Network } = this.client
const canClearCache = await Network.canClearBrowserCache
if (canClearCache) {
await Network.clearBrowserCache()
this.log(`Cache is cleared`)
} else {
this.log(`Cache could not be cleared`)
}
}
private async clearStorage(
origin: string,
storageTypes: string,
): Promise {
const { Storage, Network } = this.client
const canClearCache = await Network.canClearBrowserCache
if (canClearCache) {
await Storage.clearDataForOrigin({ origin, storageTypes })
this.log(`${storageTypes} for ${origin} is cleared`)
} else {
this.log(`${storageTypes} could not be cleared`)
}
}
private async setUserAgent(useragent: string): Promise {
this.userAgentValue = useragent
await this.log(`Set useragent to ${this.userAgentValue}`)
}
private async waitTimeout(timeout: number): Promise {
this.log(`Waiting for ${timeout}ms`)
await wait(timeout)
}
private async waitSelector(
selector: string,
waitTimeout: number = this.chromelessOptions.waitTimeout,
): Promise {
this.log(`Waiting for ${selector} ${waitTimeout}`)
await waitForNode(this.client, selector, waitTimeout)
this.log(`Waited for ${selector}`)
}
private async click(selector: string, x?: number, y?: number): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`click(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(`click(): node for selector ${selector} doesn't exist`)
}
const { scale } = this.chromelessOptions.viewport
if (this.chromelessOptions.scrollBeforeClick) {
await scrollToElement(this.client, selector)
}
await click(this.client, selector, scale, x, y)
this.log(`Clicked on ${selector} at (${x}, ${y})`)
}
private async returnCode(fn: string, ...args: any[]): Promise {
return (await evaluate(this.client, fn, ...args)) as T
}
private async scrollTo(x: number, y: number): Promise {
return scrollTo(this.client, x, y)
}
private async scrollToElement(selector: string): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`scrollToElement(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
return scrollToElement(this.client, selector)
}
private async mousedown(selector: string): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`mousedown(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(
`mousedown(): node for selector ${selector} doesn't exist`,
)
}
const { scale } = this.chromelessOptions.viewport
await mousedown(this.client, selector, scale)
this.log(`Mousedown on ${selector}`)
}
private async mouseup(selector: string): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`mouseup(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(`mouseup(): node for selector ${selector} doesn't exist`)
}
const { scale } = this.chromelessOptions.viewport
await mouseup(this.client, selector, scale)
this.log(`Mouseup on ${selector}`)
}
private async setHtml(html: string): Promise {
await setHtml(this.client, html)
}
private async focus(selector: string): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`focus(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(`focus(): node for selector ${selector} doesn't exist`)
}
await focus(this.client, selector)
this.log(`Focus on ${selector}`)
}
async type(text: string, selector?: string): Promise {
if (selector) {
if (this.chromelessOptions.implicitWait) {
this.log(`type(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(`type(): Node not found for selector: ${selector}`)
}
}
await type(this.client, text, selector)
this.log(`Typed ${text} in ${selector}`)
}
async cookies(nameOrQuery?: string | CookieQuery): Promise {
return await getCookies(this.client, nameOrQuery as string | undefined)
}
async allCookies(): Promise {
return await getAllCookies(this.client)
}
async setExtraHTTPHeaders(headers: Headers): Promise {
return await setExtraHTTPHeaders(this.client, headers)
}
async setCookies(
nameOrCookies: string | Cookie | Cookie[],
value?: string,
): Promise {
if (typeof nameOrCookies !== 'string' && !value) {
const cookies = Array.isArray(nameOrCookies)
? nameOrCookies
: [nameOrCookies]
return await setCookies(this.client, cookies)
}
if (typeof nameOrCookies === 'string' && typeof value === 'string') {
const fn = () => location.href
const url = (await evaluate(this.client, `${fn}`)) as string
const cookie: Cookie = {
url,
name: nameOrCookies,
value,
}
return await setCookies(this.client, [cookie])
}
throw new Error(`setCookies(): Invalid input ${nameOrCookies}, ${value}`)
}
async deleteCookies(name: string, url: string): Promise {
const { Network } = this.client
const canClearCookies = await Network.canClearBrowserCookies()
if (canClearCookies) {
await deleteCookie(this.client, name, url)
this.log(`Cookie ${name} cleared`)
} else {
this.log(`Cookie ${name} could not be cleared`)
}
}
async clearCookies(): Promise {
const { Network } = this.client
const canClearCookies = await Network.canClearBrowserCookies()
if (canClearCookies) {
await clearCookies(this.client)
this.log('Cookies cleared')
} else {
this.log('Cookies could not be cleared')
}
}
async press(keyCode: number, count?: number, modifiers?: any): Promise {
this.log(`Sending keyCode ${keyCode} (modifiers: ${modifiers})`)
await press(this.client, keyCode, count, modifiers)
}
async returnExists(selector: string): Promise {
return await nodeExists(this.client, selector)
}
async returnInputValue(selector: string): Promise {
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(`value: node for selector ${selector} doesn't exist`)
}
return getValue(this.client, selector)
}
// Returns the S3 url or local file path
async returnScreenshot(
selector?: string,
options?: ScreenshotOptions,
): Promise {
if (selector) {
if (this.chromelessOptions.implicitWait) {
this.log(`screenshot(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(
`screenshot(): node for selector ${selector} doesn't exist`,
)
}
}
const data = await screenshot(this.client, selector, options)
if (isS3Configured()) {
return await uploadToS3(data, 'image/png')
} else {
return writeToFile(data, 'png', options && options.filePath)
}
}
async returnHtml(): Promise {
return await html(this.client)
}
async returnHtmlUrl(options?: { filePath?: string }): Promise {
const data = await html(this.client)
if (isS3Configured()) {
return await uploadToS3(data, 'text/html')
} else {
return writeToFile(data, 'html', options && options.filePath)
}
}
// Returns the S3 url or local file path
async returnPdf(options?: PdfOptions): Promise {
const { filePath, ...cdpOptions } = options || { filePath: undefined }
const data = await pdf(this.client, cdpOptions)
if (isS3Configured()) {
return await uploadToS3(data, 'application/pdf')
} else {
return writeToFile(data, 'pdf', filePath)
}
}
async clearInput(selector: string): Promise {
if (selector) {
if (this.chromelessOptions.implicitWait) {
this.log(`clearInput(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(
`clearInput(): Node not found for selector: ${selector}`,
)
}
}
await clearInput(this.client, selector)
this.log(`${selector} cleared`)
}
async setFileInput(selector: string, files: string[]): Promise {
if (this.chromelessOptions.implicitWait) {
this.log(`setFileInput(): Waiting for ${selector}`)
await waitForNode(
this.client,
selector,
this.chromelessOptions.waitTimeout,
)
}
const exists = await nodeExists(this.client, selector)
if (!exists) {
throw new Error(
`setFileInput(): node for selector ${selector} doesn't exist`,
)
}
await setFileInput(this.client, selector, files)
this.log(`setFileInput() files ${files}`)
}
private log(msg: string): void {
if (this.chromelessOptions.debug) {
console.log(msg)
}
}
}
================================================
FILE: src/chrome/local.ts
================================================
import { Chrome, Command, ChromelessOptions, Client } from '../types'
import * as CDP from 'chrome-remote-interface'
import { LaunchedChrome, launch } from 'chrome-launcher'
import LocalRuntime from './local-runtime'
import { evaluate, setViewport } from '../util'
import { DeviceMetrics } from '../types'
interface RuntimeClient {
client: Client
runtime: LocalRuntime
}
export default class LocalChrome implements Chrome {
private options: ChromelessOptions
private runtimeClientPromise: Promise
private chromeInstance?: LaunchedChrome
constructor(options: ChromelessOptions = {}) {
this.options = options
this.runtimeClientPromise = this.initRuntimeClient()
}
private async initRuntimeClient(): Promise {
const client = this.options.launchChrome
? await this.startChrome()
: await this.connectToChrome()
const { viewport = {} as DeviceMetrics } = this.options
await setViewport(client, viewport as DeviceMetrics)
const runtime = new LocalRuntime(client, this.options)
return { client, runtime }
}
private async startChrome(): Promise {
const { port } = this.options.cdp
this.chromeInstance = await launch({
logLevel: this.options.debug ? 'info' : 'silent',
chromeFlags: [
// Do not render scroll bars
'--hide-scrollbars',
// The following options copied verbatim from https://github.com/GoogleChrome/chrome-launcher/blob/master/src/flags.ts
// Disable built-in Google Translate service
'--disable-translate',
// Disable all chrome extensions entirely
'--disable-extensions',
// Disable various background network services, including extension updating,
// safe browsing service, upgrade detector, translate, UMA
'--disable-background-networking',
// Disable fetching safebrowsing lists, likely redundant due to disable-background-networking
'--safebrowsing-disable-auto-update',
// Disable syncing to a Google account
'--disable-sync',
// Disable reporting to UMA, but allows for collection
'--metrics-recording-only',
// Mute any audio
'--mute-audio',
// Skip first run wizards
'--no-first-run',
],
port,
})
const target = await CDP.New({
port,
})
return await CDP({ target, port })
}
private async connectToChrome(): Promise {
const { host, port } = this.options.cdp
const target = await CDP.New({
port,
host,
})
return await CDP({ target, host, port })
}
private async setViewport(client: Client) {
const { viewport = {} } = this.options
const config: any = {
deviceScaleFactor: 1,
mobile: false,
scale: viewport.scale || 1,
fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable
}
const { host, port } = this.options.cdp
const versionResult = await CDP.Version({ host, port })
const isHeadless = versionResult['User-Agent'].includes('Headless')
if (viewport.height && viewport.width) {
config.height = viewport.height
config.width = viewport.width
} else if (isHeadless) {
// just apply default value in headless mode to maintain original browser viewport
config.height = 900
config.width = 1440
} else {
config.height = await evaluate(
client,
(() => window.innerHeight).toString(),
)
config.width = await evaluate(
client,
(() => window.innerWidth).toString(),
)
}
await client.Emulation.setDeviceMetricsOverride(config)
await client.Emulation.setVisibleSize({
width: config.width,
height: config.height,
})
}
async process(command: Command): Promise {
const { runtime } = await this.runtimeClientPromise
return (await runtime.run(command)) as T
}
async close(): Promise {
const { client } = await this.runtimeClientPromise
if (this.options.cdp.closeTab) {
const { host, port } = this.options.cdp
await CDP.Close({ host, port, id: client.target.id })
}
if (this.chromeInstance) {
this.chromeInstance.kill()
}
await client.close()
}
}
================================================
FILE: src/chrome/remote.ts
================================================
import { Chrome, ChromelessOptions, Command, RemoteOptions } from '../types'
import { connect as mqtt, MqttClient } from 'mqtt'
import * as cuid from 'cuid'
import * as got from 'got'
interface RemoteResult {
value?: any
error?: string
}
function getEndpoint(remoteOptions: RemoteOptions | boolean): RemoteOptions {
if (typeof remoteOptions === 'object' && remoteOptions.endpointUrl) {
return remoteOptions
}
if (
process.env['CHROMELESS_ENDPOINT_URL'] &&
process.env['CHROMELESS_ENDPOINT_API_KEY']
) {
return {
endpointUrl: process.env['CHROMELESS_ENDPOINT_URL'],
apiKey: process.env['CHROMELESS_ENDPOINT_API_KEY'],
}
}
throw new Error(
'No Chromeless remote endpoint & API key provided. Either set as "remote" option in constructor or set as "CHROMELESS_ENDPOINT_URL" and "CHROMELESS_ENDPOINT_API_KEY" env variables.',
)
}
export default class RemoteChrome implements Chrome {
private options: ChromelessOptions
private channelId: string
private channel: MqttClient
private connectionPromise: Promise
private TOPIC_NEW_SESSION: string
private TOPIC_CONNECTED: string
private TOPIC_REQUEST: string
private TOPIC_RESPONSE: string
private TOPIC_END: string
constructor(options: ChromelessOptions) {
this.options = options
this.connectionPromise = this.initConnection()
}
private async initConnection(): Promise {
await new Promise(async (resolve, reject) => {
const timeout = setTimeout(() => {
if (this.channel) {
this.channel.end()
}
reject(
new Error(
"Timed out after 30sec. Connection couldn't be established.",
),
)
}, 30000)
try {
const { endpointUrl, apiKey } = getEndpoint(this.options.remote)
const { body: { url, channelId } } = await got(endpointUrl, {
headers: apiKey
? {
'x-api-key': apiKey,
}
: undefined,
json: true,
})
this.channelId = channelId
this.TOPIC_NEW_SESSION = 'chrome/new-session'
this.TOPIC_CONNECTED = `chrome/${channelId}/connected`
this.TOPIC_REQUEST = `chrome/${channelId}/request`
this.TOPIC_RESPONSE = `chrome/${channelId}/response`
this.TOPIC_END = `chrome/${channelId}/end`
const channel = mqtt(url, {
will: {
topic: 'chrome/last-will',
payload: JSON.stringify({ channelId }),
qos: 1,
retain: false,
},
})
this.channel = channel
if (this.options.debug) {
channel.on('error', error => console.log('WebSocket error', error))
channel.on('offline', () => console.log('WebSocket offline'))
}
channel.on('connect', () => {
if (this.options.debug) {
console.log('Connected to message broker.')
}
channel.subscribe(this.TOPIC_CONNECTED, { qos: 1 }, () => {
channel.on('message', async topic => {
if (this.TOPIC_CONNECTED === topic) {
clearTimeout(timeout)
resolve()
}
})
channel.publish(
this.TOPIC_NEW_SESSION,
JSON.stringify({ channelId, options: this.options }),
{ qos: 1 },
)
})
channel.subscribe(this.TOPIC_END, () => {
channel.on('message', async (topic, buffer) => {
if (this.TOPIC_END === topic) {
const message = buffer.toString()
const data = JSON.parse(message)
if (data.outOfTime) {
console.warn(
`Chromeless Proxy disconnected because it reached it's execution time limit (5 minutes).`,
)
} else if (data.inactivity) {
console.warn(
'Chromeless Proxy disconnected due to inactivity (no commands sent for 30 seconds).',
)
} else {
console.warn(
`Chromeless Proxy disconnected (we don't know why).`,
data,
)
}
await this.close()
}
})
})
})
} catch (error) {
console.error(error)
reject(
new Error('Unable to get presigned websocket URL and connect to it.'),
)
}
})
}
async process(command: Command): Promise {
// wait until lambda connection is established
await this.connectionPromise
if (this.options.debug) {
console.log(`Running remotely: ${JSON.stringify(command)}`)
}
const promise = new Promise((resolve, reject) => {
this.channel.subscribe(this.TOPIC_RESPONSE, () => {
this.channel.on('message', (topic, buffer) => {
if (this.TOPIC_RESPONSE === topic) {
const message = buffer.toString()
const result = JSON.parse(message) as RemoteResult
if (result.error) {
reject(result.error)
} else if (result.value) {
resolve(result.value)
} else {
resolve()
}
}
})
this.channel.publish(this.TOPIC_REQUEST, JSON.stringify(command))
})
})
return promise
}
async close(): Promise {
this.channel.publish(
this.TOPIC_END,
JSON.stringify({ channelId: this.channelId, client: true }),
)
this.channel.end()
}
}
================================================
FILE: src/index.ts
================================================
import Chromeless from './api'
import Queue from './queue'
import LocalChrome from './chrome/local'
import { version } from './util'
import { Cookie, ChromelessOptions } from './types'
export { Queue, LocalChrome, Chromeless, Cookie, ChromelessOptions, version }
export default Chromeless
================================================
FILE: src/queue.ts
================================================
import { Chrome, Command } from './types'
export default class Queue {
private flushCount: number
private commandQueue: {
[flushCount: number]: Command[]
}
private chrome: Chrome
private lastWaitAll: Promise
constructor(chrome: Chrome) {
this.chrome = chrome
this.flushCount = 0
this.commandQueue = {
0: [],
}
}
async end(): Promise {
this.lastWaitAll = this.waitAll()
await this.lastWaitAll
await this.chrome.close()
}
enqueue(command: Command): void {
this.commandQueue[this.flushCount].push(command)
}
async process(command: Command): Promise {
// with lastWaitAll we build a promise chain
// already change the pointer to lastWaitAll for the next .process() call
// after the pointer is set, wait for the previous tasks
// then wait for the own pointer (the new .lastWaitAll)
if (this.lastWaitAll) {
const lastWaitAllTmp = this.lastWaitAll
this.lastWaitAll = this.waitAll()
await lastWaitAllTmp
} else {
this.lastWaitAll = this.waitAll()
}
await this.lastWaitAll
return this.chrome.process(command)
}
private async waitAll(): Promise {
const previousFlushCount = this.flushCount
this.flushCount++
this.commandQueue[this.flushCount] = []
for (const command of this.commandQueue[previousFlushCount]) {
await this.chrome.process(command)
}
}
}
================================================
FILE: src/types.ts
================================================
export interface Client {
Network: any
Page: any
DOM: any
Input: any
Runtime: any
Emulation: any
Storage: any
close: () => void
target: {
id: string
}
port: any
host: any
}
export interface DeviceMetrics {
width: number
height: number
deviceScaleFactor?: number
mobile?: boolean
scale?: number
screenOrientation?: ScreenOrientation
}
export interface ScreenOrientation {
type: string
angle: number
}
export interface RemoteOptions {
endpointUrl: string
apiKey?: string
}
export interface CDPOptions {
host?: string // localhost
port?: number // 9222
secure?: boolean // false
closeTab?: boolean // true
}
export interface ChromelessOptions {
debug?: boolean // false
waitTimeout?: number // 10000ms
implicitWait?: boolean // false
scrollBeforeClick?: boolean // false
viewport?: {
width?: number // 1440 if headless
height?: number // 900 if headless
scale?: number // 1
}
launchChrome?: boolean // auto-launch chrome (local) `true`
cdp?: CDPOptions
remote?: RemoteOptions | boolean
}
export interface Chrome {
process(command: Command): Promise
close(): Promise
}
export type Command =
| {
type: 'goto'
url: string
timeout?: number
}
| {
type: 'clearCache'
}
| {
type: 'setViewport'
options: DeviceMetrics
}
| {
type: 'setUserAgent'
useragent: string
}
| {
type: 'wait'
timeout?: number
selector?: string
fn?: string
args?: any[]
}
| {
type: 'click'
selector: string
x?: number
y?: number
}
| {
type: 'returnCode'
fn: string
args?: any[]
}
| {
type: 'returnInputValue'
selector: string
}
| {
type: 'returnExists'
selector: string
}
| {
type: 'returnValue'
selector: string
}
| {
type: 'returnScreenshot'
selector?: string
options?: ScreenshotOptions
}
| {
type: 'returnHtml'
}
| {
type: 'returnHtmlUrl'
}
| {
type: 'returnPdf'
options?: PdfOptions
}
| {
type: 'scrollTo'
x: number
y: number
}
| {
type: 'scrollToElement'
selector: string
}
| {
type: 'setHtml'
html: string
}
| {
type: 'setExtraHTTPHeaders'
headers: Headers
}
| {
type: 'press'
keyCode: number
count?: number
modifiers?: any
}
| {
type: 'type'
input: string
selector?: string
}
| {
type: 'clearCookies'
}
| {
type: 'clearStorage'
origin: string
storageTypes: string
}
| {
type: 'deleteCookies'
name: string
url: string
}
| {
type: 'setCookies'
nameOrCookies: string | Cookie | Cookie[]
value?: string
}
| {
type: 'allCookies'
}
| {
type: 'cookies'
nameOrQuery?: string | CookieQuery
}
| {
type: 'mousedown'
selector: string
}
| {
type: 'mouseup'
selector: string
}
| {
type: 'focus'
selector: string
}
| {
type: 'clearInput'
selector: string
}
| {
type: 'setFileInput'
selector: string
files: string[]
}
export type Headers = Record
export interface Cookie {
url?: string
domain?: string
name: string
value: string
path?: string
expires?: number
size?: number
httpOnly?: boolean
secure?: boolean
session?: boolean
}
export interface CookieQuery {
name: string
path?: string
expires?: number
size?: number
httpOnly?: boolean
secure?: boolean
session?: boolean
}
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
export interface PdfOptions {
landscape?: boolean
displayHeaderFooter?: boolean
printBackground?: boolean
scale?: number
paperWidth?: number
paperHeight?: number
marginTop?: number
marginBottom?: number
marginLeft?: number
marginRight?: number
pageRanges?: string
ignoreInvalidPageRanges?: boolean
filePath?: string // for internal use
}
export interface ScreenshotOptions {
filePath?: string
omitBackground?: boolean
}
export type Quad = Array
export interface ShapeOutsideInfo {
bounds: Quad
shape: Array
marginShape: Array
}
export interface BoxModel {
content: Quad
padding: Quad
border: Quad
margin: Quad
width: number
height: number
shapeOutside: ShapeOutsideInfo
}
export interface Viewport {
x: number
y: number
width: number
height: number
scale: number
}
================================================
FILE: src/util.test.ts
================================================
import * as fs from 'fs'
import * as os from 'os'
import * as CDP from 'chrome-remote-interface'
import test from 'ava'
import Chromeless from '../src'
const testHtml = fs.readFileSync('./src/__tests__/test.html')
const testUrl = `data:text/html,${testHtml}`
const getPngMetaData = async (filePath): Promise => {
const fd = fs.openSync(filePath, 'r')
return await new Promise(resolve => {
fs.read(fd, Buffer.alloc(24), 0, 24, 0, (err, bytesRead, buffer) =>
resolve({
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20),
}),
)
})
}
// POC
test('evaluate (document.title)', async t => {
const chromeless = new Chromeless({ launchChrome: false })
const title = await chromeless.goto(testUrl).evaluate(() => document.title)
await chromeless.end()
t.is(title, 'Title')
})
test('screenshot and pdf path', async t => {
const chromeless = new Chromeless({ launchChrome: false })
const screenshot = await chromeless.goto(testUrl).screenshot()
const pdf = await chromeless.goto(testUrl).pdf()
await chromeless.end()
const regex = new RegExp(os.tmpdir().replace(/\\/g, '\\\\'))
t.regex(screenshot, regex)
t.regex(pdf, regex)
})
test('screenshot by selector', async t => {
const version = await CDP.Version()
const versionMajor = parseInt(/Chrome\/(\d+)/.exec(version['User-Agent'])[1])
// clipping will only work on chrome 61+
const chromeless = new Chromeless({ launchChrome: false })
const screenshot = await chromeless.goto(testUrl).screenshot('img')
await chromeless.end()
const png = await getPngMetaData(screenshot)
t.is(png.width, versionMajor > 60 ? 512 : 1440)
t.is(png.height, versionMajor > 60 ? 512 : 900)
})
================================================
FILE: src/util.ts
================================================
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as cuid from 'cuid'
import {
Client,
Cookie,
DeviceMetrics,
PdfOptions,
BoxModel,
Viewport,
Headers,
ScreenshotOptions,
} from './types'
import * as CDP from 'chrome-remote-interface'
import * as AWS from 'aws-sdk'
export const version: string = ((): string => {
if (fs.existsSync(path.join(__dirname, '../package.json'))) {
// development (look in /src)
return require('../package.json').version
} else {
// production (look in /dist/src)
return require('../../package.json').version
}
})()
export async function setViewport(
client: Client,
viewport: DeviceMetrics = { width: 1, height: 1, scale: 1 },
): Promise {
const config: any = {
deviceScaleFactor: 1,
mobile: false,
scale: viewport.scale || 1,
fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable
}
const { host, port } = client
const versionResult = await CDP.Version({ host, port })
const isHeadless = versionResult['User-Agent'].includes('Headless')
if (viewport.height && viewport.width) {
config.height = viewport.height
config.width = viewport.width
} else if (isHeadless) {
// just apply default value in headless mode to maintain original browser viewport
config.height = 900
config.width = 1440
} else {
config.height = await evaluate(
client,
(() => window.innerHeight).toString(),
)
config.width = await evaluate(client, (() => window.innerWidth).toString())
}
await client.Emulation.setDeviceMetricsOverride(config)
await client.Emulation.setVisibleSize({
width: config.width,
height: config.height,
})
return
}
export async function waitForNode(
client: Client,
selector: string,
waitTimeout: number,
): Promise {
const { Runtime } = client
const getNode = `selector => {
return document.querySelector(selector)
}`
const result = await Runtime.evaluate({
expression: `(${getNode})(\`${selector}\`)`,
})
if (result.result.value === null) {
const start = new Date().getTime()
return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
if (new Date().getTime() - start > waitTimeout) {
clearInterval(interval)
reject(
new Error(`wait("${selector}") timed out after ${waitTimeout}ms`),
)
}
const result = await Runtime.evaluate({
expression: `(${getNode})(\`${selector}\`)`,
})
if (result.result.value !== null) {
clearInterval(interval)
resolve()
}
}, 500)
})
} else {
return
}
}
export async function wait(timeout: number): Promise {
return new Promise((resolve, reject) => setTimeout(resolve, timeout))
}
export async function waitForPromise(
promise: Promise,
waitTimeout: number,
label?: string,
): Promise {
return new Promise((resolve, reject) => {
let fullfilled = false
setTimeout(() => {
fullfilled = true
reject(
new Error(
`wait(${label || 'Promise'}) timed out after ${waitTimeout}ms`,
),
)
}, waitTimeout)
return promise
.then(res => (fullfilled ? void 0 : resolve(res)))
.catch(err => (fullfilled ? void 0 : reject(err)))
})
}
export function eventToPromise() {
let resolve
const promise = new Promise(res => {
resolve = res
})
return {
onEvent(...args) {
resolve(args.length > 1 ? args : args[0])
},
fired() {
return promise
},
}
}
export async function nodeExists(
client: Client,
selector: string,
): Promise {
const { Runtime } = client
const exists = `selector => {
return !!document.querySelector(selector)
}`
const expression = `(${exists})(\`${selector}\`)`
const result = await Runtime.evaluate({
expression,
})
return result.result.value
}
export async function getClientRect(client, selector): Promise {
const { Runtime } = client
const code = `selector => {
const element = document.querySelector(selector)
if (!element) {
return undefined
}
const rect = element.getBoundingClientRect()
return JSON.stringify({
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
height: rect.height,
width: rect.width,
})
}`
const expression = `(${code})(\`${selector}\`)`
const result = await Runtime.evaluate({ expression })
if (!result.result.value) {
throw new Error(`No element found for selector: ${selector}`)
}
return JSON.parse(result.result.value) as ClientRect
}
export async function click(
client: Client,
selector: string,
scale: number,
x?: number,
y?: number,
) {
const clientRect = await getClientRect(client, selector)
const { Input } = client
if (x === undefined) x = clientRect.width / 2
if (y === undefined) y = clientRect.height / 2
const options = {
x: Math.round((clientRect.left + x) * scale),
y: Math.round((clientRect.top + y) * scale),
button: 'left',
clickCount: 1,
}
await Input.dispatchMouseEvent({
...options,
type: 'mousePressed',
})
await Input.dispatchMouseEvent({
...options,
type: 'mouseReleased',
})
}
export async function focus(client: Client, selector: string): Promise {
const { DOM } = client
const dom = await DOM.getDocument()
const node = await DOM.querySelector({
nodeId: dom.root.nodeId,
selector: selector,
})
await DOM.focus(node)
}
export async function evaluate(
client: Client,
fn: string,
...args: any[]
): Promise {
const { Runtime } = client
const jsonArgs = JSON.stringify(args)
const argStr = jsonArgs.substr(1, jsonArgs.length - 2)
const expression = `
(() => {
const expressionResult = (${fn})(${argStr});
if (expressionResult && expressionResult.then) {
expressionResult.catch((error) => { throw new Error(error); });
return expressionResult;
}
return Promise.resolve(expressionResult);
})();
`
const result = await Runtime.evaluate({
expression,
returnByValue: true,
awaitPromise: true,
})
if (result && result.exceptionDetails) {
throw new Error(
result.exceptionDetails.exception.value ||
result.exceptionDetails.exception.description,
)
}
if (result && result.result) {
return result.result.value
}
return null
}
export async function type(
client: Client,
text: string,
selector?: string,
): Promise {
if (selector) {
await focus(client, selector)
await wait(500)
}
const { Input } = client
for (let i = 0; i < text.length; i++) {
const char = text[i]
const options = {
type: 'char',
text: char,
unmodifiedText: char,
}
await Input.dispatchKeyEvent(options)
}
}
export async function press(
client: Client,
keyCode: number,
count?: number,
modifiers?: any,
): Promise {
const { Input } = client
if (count === undefined) {
count = 1
}
const options = {
nativeVirtualKeyCode: keyCode,
windowsVirtualKeyCode: keyCode,
}
if (modifiers) {
options['modifiers'] = modifiers
}
for (let i = 0; i < count; i++) {
await Input.dispatchKeyEvent({
...options,
type: 'rawKeyDown',
})
await Input.dispatchKeyEvent({
...options,
type: 'keyUp',
})
}
}
export async function getValue(
client: Client,
selector: string,
): Promise {
const { Runtime } = client
const browserCode = `selector => {
return document.querySelector(selector).value
}`
const expression = `(${browserCode})(\`${selector}\`)`
const result = await Runtime.evaluate({
expression,
})
return result.result.value
}
export async function scrollTo(
client: Client,
x: number,
y: number,
): Promise {
const { Runtime } = client
const browserCode = `(x, y) => {
return window.scrollTo(x, y)
}`
const expression = `(${browserCode})(${x}, ${y})`
await Runtime.evaluate({
expression,
})
}
export async function scrollToElement(
client: Client,
selector: string,
): Promise {
const clientRect = await getClientRect(client, selector)
return scrollTo(client, clientRect.left, clientRect.top)
}
export async function setHtml(client: Client, html: string): Promise {
const { Page } = client
const { frameTree: { frame: { id: frameId } } } = await Page.getResourceTree()
await Page.setDocumentContent({ frameId, html })
}
export async function getCookies(
client: Client,
nameOrQuery?: string | Cookie,
): Promise {
const { Network } = client
const fn = () => location.href
const url = (await evaluate(client, `${fn}`)) as string
const result = await Network.getCookies([url])
const cookies = result.cookies
if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery === 'string') {
const filteredCookies: Cookie[] = cookies.filter(
cookie => cookie.name === nameOrQuery,
)
return filteredCookies
}
return cookies
}
export async function getAllCookies(client: Client): Promise {
const { Network } = client
const result = await Network.getAllCookies()
return result.cookies
}
export async function setCookies(
client: Client,
cookies: Cookie[],
): Promise {
const { Network } = client
for (const cookie of cookies) {
await Network.setCookie({
...cookie,
url: cookie.url ? cookie.url : getUrlFromCookie(cookie),
})
}
}
export async function setExtraHTTPHeaders(
client: Client,
headers: Headers,
): Promise {
const { Network } = client
await Network.setExtraHTTPHeaders({ headers })
}
export async function mousedown(
client: Client,
selector: string,
scale: number,
) {
const clientRect = await getClientRect(client, selector)
const { Input } = client
const options = {
x: Math.round((clientRect.left + clientRect.width / 2) * scale),
y: Math.round((clientRect.top + clientRect.height / 2) * scale),
button: 'left',
clickCount: 1,
}
await Input.dispatchMouseEvent({
...options,
type: 'mousePressed',
})
}
export async function mouseup(client: Client, selector: string, scale: number) {
const clientRect = await getClientRect(client, selector)
const { Input } = client
const options = {
x: Math.round((clientRect.left + clientRect.width / 2) * scale),
y: Math.round((clientRect.top + clientRect.height / 2) * scale),
button: 'left',
clickCount: 1,
}
await Input.dispatchMouseEvent({
...options,
type: 'mouseReleased',
})
}
function getUrlFromCookie(cookie: Cookie) {
const domain = cookie.domain.slice(1, cookie.domain.length)
return `https://${domain}`
}
export async function deleteCookie(
client: Client,
name: string,
url: string,
): Promise {
const { Network } = client
await Network.deleteCookie({ cookieName: name, url })
}
export async function clearCookies(client: Client): Promise {
const { Network } = client
await Network.clearBrowserCookies()
}
export async function getBoxModel(
client: Client,
selector: string,
): Promise {
const { DOM } = client
const { root: { nodeId: documentNodeId } } = await DOM.getDocument()
const { nodeId } = await DOM.querySelector({
selector: selector,
nodeId: documentNodeId,
})
const { model } = await DOM.getBoxModel({ nodeId })
return model
}
export function boxModelToViewPort(model: BoxModel, scale: number): Viewport {
return {
x: model.content[0],
y: model.content[1],
width: model.width,
height: model.height,
scale,
}
}
export async function screenshot(
client: Client,
selector: string,
options: ScreenshotOptions,
): Promise {
const { Page } = client
const captureScreenshotOptions = {
format: 'png',
fromSurface: true,
clip: undefined,
}
if (selector) {
const model = await getBoxModel(client, selector)
captureScreenshotOptions.clip = boxModelToViewPort(model, 1)
}
if (options && options.omitBackground)
client.Emulation.setDefaultBackgroundColorOverride({
color: { r: 0, g: 0, b: 0, a: 0 },
})
const screenshot = await Page.captureScreenshot(captureScreenshotOptions)
if (options && options.omitBackground)
client.Emulation.setDefaultBackgroundColorOverride()
return screenshot.data
}
export async function html(client: Client): Promise {
const { DOM } = client
const { root: { nodeId } } = await DOM.getDocument()
const { outerHTML } = await DOM.getOuterHTML({ nodeId })
return outerHTML
}
export async function htmlUrl(client: Client): Promise {
const { DOM } = client
const { root: { nodeId } } = await DOM.getDocument()
const { outerHTML } = await DOM.getOuterHTML({ nodeId })
return outerHTML
}
export async function pdf(
client: Client,
options?: PdfOptions,
): Promise {
const { Page } = client
const pdf = await Page.printToPDF(options)
return pdf.data
}
export async function clearInput(
client: Client,
selector: string,
): Promise {
await wait(500)
await focus(client, selector)
const { Input } = client
const text = await getValue(client, selector)
const optionsDelete = {
nativeVirtualKeyCode: 46,
windowsVirtualKeyCode: 46,
}
const optionsBackspace = {
nativeVirtualKeyCode: 8,
windowsVirtualKeyCode: 8,
}
for (let i = 0; i < text.length; i++) {
await Input.dispatchKeyEvent({
...optionsDelete,
type: 'rawKeyDown',
})
Input.dispatchKeyEvent({
...optionsDelete,
type: 'keyUp',
})
await Input.dispatchKeyEvent({
...optionsBackspace,
type: 'rawKeyDown',
})
Input.dispatchKeyEvent({
...optionsBackspace,
type: 'keyUp',
})
}
}
export async function setFileInput(
client: Client,
selector: string,
files: string[],
): Promise {
const { DOM } = client
const dom = await DOM.getDocument()
const node = await DOM.querySelector({
nodeId: dom.root.nodeId,
selector: selector,
})
return await DOM.setFileInputFiles({ files: files, nodeId: node.nodeId })
}
export function getDebugOption(): boolean {
if (
process &&
process.env &&
process.env['DEBUG'] &&
process.env['DEBUG'].includes('chromeless')
) {
return true
}
return false
}
export function writeToFile(
data: string,
extension: string,
filePathOverride: string,
): string {
const filePath =
filePathOverride || path.join(os.tmpdir(), `${cuid()}.${extension}`)
fs.writeFileSync(filePath, Buffer.from(data, 'base64'))
return filePath
}
function getS3BucketName() {
return process.env['CHROMELESS_S3_BUCKET_NAME']
}
function getS3BucketUrl() {
return process.env['CHROMELESS_S3_BUCKET_URL']
}
function getS3ObjectKeyPrefix() {
return process.env['CHROMELESS_S3_OBJECT_KEY_PREFIX'] || ''
}
function getS3FilesPermissions() {
return process.env['CHROMELESS_S3_OBJECT_ACL'] || 'public-read'
}
export function isS3Configured() {
return getS3BucketName() && getS3BucketUrl()
}
const s3ContentTypes = {
'image/png': {
extension: 'png',
},
'application/pdf': {
extension: 'pdf',
},
'text/html': {
extension: 'html',
},
}
export async function uploadToS3(
data: string,
contentType: string,
): Promise {
const s3ContentType = s3ContentTypes[contentType]
if (!s3ContentType) {
throw new Error(`Unknown S3 Content type ${contentType}`)
}
const s3Path = `${getS3ObjectKeyPrefix()}${cuid()}.${s3ContentType.extension}`
const s3 = new AWS.S3()
await s3
.putObject({
Bucket: getS3BucketName(),
Key: s3Path,
ContentType: contentType,
ACL: getS3FilesPermissions(),
Body: Buffer.from(data, contentType === 'text/html' ? 'utf8' : 'base64'),
})
.promise()
return `https://${getS3BucketUrl()}/${s3Path}`
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"lib": ["es6", "dom"],
"strictNullChecks": false,
"pretty": true,
"rootDir": ".",
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "node_modules/**", "dist"],
"include": ["./src/**/*.ts", "./examples/**/*.ts"]
}
================================================
FILE: tslint.json
================================================
{
"rules": {
"class-name": true,
"comment-format": [true, "check-space"],
"no-var-keyword": true,
"no-internal-module": true,
"no-null-keyword": false,
"prefer-const": true,
"jsdoc-format": true
}
}