Full Code of schickling/chromeless for AI

master 774223e4e7f6 cached
40 files
141.0 KB
45.2k tokens
160 symbols
1 requests
Download .txt
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
================================================
<!--

The issue tracker is only for bug reports or feature requests.

1. If you have a question and not a bug/feature request please ask it on StackOverflow here: https://stackoverflow.com/questions/ask?tags=chromeless
    Non-bug/feature request issues will be closed. The reason for this is that, the issue tracker can quickly become filled up with requests-for-help which becomes difficult (and overwhelming) for project collaborators to manage.
2. Please search for and check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/graphcool/chromeless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# 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
================================================
<!--
  Thanks for filing a pull request for Chromeless!

  Please look at the following checklist to ensure that your PR
  can be accepted quickly. Once all the items are checked-off (and CircleCI is passing), we will review your PR:
-->

- [ ] 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

[![npm](https://img.shields.io/npm/v/chromeless.svg)](https://npmjs.com/package/chromeless)
[![downloads](https://img.shields.io/npm/dm/chromeless.svg)](https://npmjs.com/package/chromeless)
[![circleci](https://circleci.com/gh/prismagraphql/chromeless.svg?style=shield)](https://circleci.com/gh/prismagraphql/workflows/chromeless/tree/master)
[![codecov](https://codecov.io/gh/prismagraphql/chromeless/branch/master/graph/badge.svg)](https://codecov.io/gh/prismagraphql/chromeless)
[![dependencies](https://david-dm.org/prismagraphql/chromeless/status.svg)](https://david-dm.org/prismagraphql/chromeless)
[![node](https://img.shields.io/node/v/chromeless.svg)]()
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](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)).

[![](http://i.imgur.com/i1gtCzy.png)](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

![](http://imgur.com/2bgTyAi.png)

### 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<U extends any>(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 💚

<a href="https://github.com/joelgriffith/" target="_blank">
  <img src="https://github.com/joelgriffith.png?size=64" width="64" height="64" alt="joelgriffith">
</a>
<a href="https://github.com/adieuadieu/" target="_blank">
  <img src="https://github.com/adieuadieu.png?size=64" width="64" height="64" alt="adieuadieu">
</a>
<a href="https://github.com/schickling/" target="_blank">
  <img src="https://github.com/schickling.png?size=64" width="64" height="64" alt="schickling">
</a>
<a href="https://github.com/timsuchanek/" target="_blank">
  <img src="https://github.com/timsuchanek.png?size=64" width="64" height="64" alt="timsuchanek">
</a>


<a href="https://github.com/Chrisgozd/" target="_blank">
  <img src="https://github.com/Chrisgozd.png?size=64" width="64" height="64" alt="Chrisgozd">
</a>
<a href="https://github.com/criticalbh/" target="_blank">
  <img src="https://github.com/criticalbh.png?size=64" width="64" height="64" alt="criticalbh">
</a>
<a href="https://github.com/d2s/" target="_blank">
  <img src="https://github.com/d2s.png?size=64" width="64" height="64" alt="d2s">
</a>
<a href="https://github.com/emeth-/" target="_blank">
  <img src="https://github.com/emeth-.png?size=64" width="64" height="64" alt="emeth-">
</a>
<a href="https://github.com/githubixx/" target="_blank">
  <img src="https://github.com/githubixx.png?size=64" width="64" height="64" alt="githubixx">
</a>
<a href="https://github.com/hax/" target="_blank">
  <img src="https://github.com/hax.png?size=64" width="64" height="64" alt="hax">
</a>
<a href="https://github.com/Hazealign/" target="_blank">
  <img src="https://github.com/Hazealign.png?size=64" width="64" height="64" alt="Hazealign">
</a>
<a href="https://github.com/joeyvandijk/" target="_blank">
  <img src="https://github.com/joeyvandijk.png?size=64" width="64" height="64" alt="joeyvandijk">
</a>
<a href="https://github.com/liady/" target="_blank">
  <img src="https://github.com/liady.png?size=64" width="64" height="64" alt="liady">
</a>
<a href="https://github.com/matthewmueller/" target="_blank">
  <img src="https://github.com/matthewmueller.png?size=64" width="64" height="64" alt="matthewmueller">
</a>
<a href="https://github.com/seangransee/" target="_blank">
  <img src="https://github.com/seangransee.png?size=64" width="64" height="64" alt="seangransee">
</a>
<a href="https://github.com/sorenbs/" target="_blank">
  <img src="https://github.com/sorenbs.png?size=64" width="64" height="64" alt="sorenbs">
</a>
<a href="https://github.com/toddwprice/" target="_blank">
  <img src="https://github.com/toddwprice.png?size=64" width="64" height="64" alt="toddwprice">
</a>
<a href="https://github.com/vladgolubev/" target="_blank">
  <img src="https://github.com/vladgolubev.png?size=64" width="64" height="64" alt="vladgolubev">
</a>




## 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


<a name="help-and-community" />

## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](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!

<p align="center"><a href="https://oss.prisma.io"><img src="https://imgur.com/IMU2ERq.png" alt="Prisma" height="170px"></a></p>


================================================
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<U extends any>(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)


---------------------------------------

<a name="api-end" />

### end(): Promise<T>

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()
```

---------------------------------------

<a name="api-goto" />

### goto(url: string, timeout?: number): Chromeless<T>

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/')
```

---------------------------------------

<a name="api-setuseragent" />

### setUserAgent(useragent: string): Chromeless<T>

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')
```

---------------------------------------

<a name="api-click" />

### click(selector: string, x?: number, y?: number): Chromeless<T>

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)
```

---------------------------------------

<a name="api-wait-timeout" />

### wait(timeout: number): Chromeless<T>

Wait for some duration. Useful for waiting for things download.

__Arguments__
- `timeout` - How long to wait, in ms

__Example__

```js
await chromeless.wait(1000)
```

---------------------------------------

<a name="api-wait-selector" />

### wait(selector: string, timeout?: number): Chromeless<T>

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)
```

---------------------------------------

<a name="api-wait-fn" />

### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless<T>

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();
  });
})
```

---------------------------------------

<a name="api-clearcache" />

### clearCache(): Chromeless<T>

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()
```

---------------------------------------

<a name="api-clearstorage" />

### clearStorage(origin: string, storageTypes: string): Chromeless<T>

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')
```

---------------------------------------

<a name="api-focus" />

### focus(selector: string): Chromeless<T>

Provide focus on a DOM element.

__Arguments__
- `selector` - DOM selector to focus

__Example__

```js
await chromeless.focus('input#searchField')
```

---------------------------------------

<a name="api-press" />

### press(keyCode: number, count?: number, modifiers?: any): Chromeless<T>

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)
```

---------------------------------------

<a name="api-type" />

### type(input: string, selector?: string): Chromeless<T>

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"]')
```

---------------------------------------

<a name="api-back" />

### back() - Not implemented yet

Not implemented yet

---------------------------------------

<a name="api-forward" />

### forward() - Not implemented yet

Not implemented yet

---------------------------------------

<a name="api-refresh" />

### refresh() - Not implemented yet

Not implemented yet

---------------------------------------

<a name="api-mousedown" />

### mousedown(selector: string): Chromeless<T>

Send mousedown event on something in the DOM.

__Arguments__
- `selector` - DOM selector for element to send mousedown event

__Example__

```js
await chromeless.mousedown('#item')
```

---------------------------------------

<a name="api-mouseup" />

### mouseup(selector: string): Chromeless<T>

Send mouseup event on something in the DOM.

__Arguments__
- `selector` - DOM selector for element to send mouseup event

__Example__

```js
await chromeless.mouseup('#placeholder')
```

---------------------------------------

<a name="api-scrollto" />

### scrollTo(x: number, y: number): Chromeless<T>

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)
```

---------------------------------------

<a name="api-scrolltoelement" />

### scrollToElement(selector: string): Chromeless<T>

Scroll to location of element. Behavior is simiar to `<a href="#fragment"></a>` — target element will be at the top of viewport

__Arguments__
- `selector` - DOM selector for element to scroll to

__Example__

  ```js
await chromeless.scrollToElement('.button')
  ```

  ---------------------------------------

<a name="api-sethtml" />

### setHtml(html: string): Chromeless<T>

Sets given markup as the document's HTML.

__Arguments__
- `html` - HTML to set as the document's markup.

__Example__

```js
await chromeless.setHtml('<h1>Hello world!</h1>')
```

  ---------------------------------------

<a name="api-setextrahttpheaders" />

### setExtraHTTPHeaders(headers: Headers): Chromeless<T>

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'
})
```


---------------------------------------

<a name="api-setviewport" />

### 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})
```

---------------------------------------

<a name="api-evaluate" />

### evaluate<U extends any>(fn: (...args: any[]) => void, ...args: any[]): Chromeless<U>

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)
  })
```

---------------------------------------

<a name="api-inputvalue" />

### inputValue(selector: string): Chromeless<string>

Get the value of an input field.

__Arguments__
- `selector` - DOM input element

__Example__

```js
await chromeless.inputValue('input#searchField')
```

---------------------------------------

<a name="api-exists" />

### exists(selector: string): Chromeless<boolean>

Test if a DOM element exists in the document.

__Arguments__
- `selector` - DOM element to check for

__Example__

```js
await chromeless.exists('div#ready')
```

---------------------------------------

<a name="api-screenshot" />

### screenshot(selector: string, options: ScreenshotOptions): Chromeless<string>

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
```

---------------------------------------

<a name="api-pdf" />

### pdf(options?: PdfOptions) - Chromeless<string>

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
```

---------------------------------------

<a name="api-html" />

### html(): Chromeless<string>

Get full HTML of the loaded page.

__Example__

```js
const html = await chromeless
  .setHtml('<h1>Hello world!</h1>')
  .html()

console.log(html) // <html><head></head><body><h1>Hello world!</h1></body></html>
```

---------------------------------------

<a name="api-cookies" />

### cookies(): Chromeless<Cookie[] | null>

Returns all browser cookies for the current URL.

__Example__

```js
await chromeless.cookies()
```

---------------------------------------

<a name="api-cookies-name" />

### cookies(name: string): Chromeless<Cookie | null>

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')
```

---------------------------------------

<a name="api-cookies-query" />

### cookies(query: CookieQuery) - Not implemented yet

Not implemented yet

---------------------------------------

<a name="api-all-cookies" />

### allCookies(): Chromeless<Cookie[]>

Returns all browser cookies. Nam nom nom.

__Example__

```js
await chromeless.allCookies()
```

---------------------------------------

<a name="api-setcookies" />

### setCookies(name: string, value: string): Chromeless<T>

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')
```

---------------------------------------

<a name="api-setcookies-one" />

### setCookies(cookie: Cookie): Chromeless<T>

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,
})
```

---------------------------------------

<a name="api-setcookies-many" />

### setCookies(cookies: Cookie[]): Chromeless<T>

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,
  }
])
```

---------------------------------------

<a name="api-deletecookies" />

### deleteCookies(name: string) - Not implemented yet

Delete a specific cookie.

__Arguments__
- `name` - name of the cookie

__Example__

```js
await chromeless.deleteCookies('cookieName')
```

---------------------------------------

<a name="api-clearcookies" />

### clearCookies(): Chromeless<T>

Clears all browser cookies.

__Example__

```js
await chromeless.clearCookies()
```
---------------------------------------

<a name="api-clearInput" />

### clearInput(selector: string): Chromeless<T>

Clear input text.


__Example__

```js
await chromeless.clearInput('#username')
```
---------------------------------------

<a name="api-set-file-input" />

### setFileInput(selector: string, files: string | string[]): Chromeless<T>

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=<your-profile-name>
```

or

```bash
export AWS_ACCESS_KEY_ID=<your-key-here>
export AWS_SECRET_ACCESS_KEY=<your-secret-key-here>
```

### 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<void> => {
  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<void> => {
  // 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<void> => {
  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<void> => {
  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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="logo"><img src="data:image/png;base64,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" alt="Chromeless" /></div>
    <div>
        <p>This is a test page for Chromeless unit tests</p>
    </div>
</div>
</body>
</html>

================================================
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<T extends any> implements Promise<T> {
  private queue: Queue
  private lastReturnPromise: Promise<any>

  constructor(options: ChromelessOptions = {}, copyInstance?: Chromeless<any>) {
    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<U>(
    onFulfill?: ((value: T) => U | PromiseLike<U>) | null,
    onReject?: ((error: any) => U | PromiseLike<U>) | null,
  ): Promise<U> {
    return this.lastReturnPromise.then(onFulfill, onReject) as Promise<U>
  }

  catch<U>(onrejected?: (reason: any) => U | PromiseLike<U>): Promise<U> {
    return this.lastReturnPromise.catch(onrejected) as Promise<U>
  }

  goto(url: string, timeout?: number): Chromeless<T> {
    this.queue.enqueue({ type: 'goto', url, timeout })

    return this
  }

  setUserAgent(useragent: string): Chromeless<T> {
    this.queue.enqueue({ type: 'setUserAgent', useragent })

    return this
  }

  click(selector: string, x?: number, y?: number): Chromeless<T> {
    this.queue.enqueue({ type: 'click', selector, x, y })

    return this
  }

  wait(timeout: number): Chromeless<T>
  wait(selector: string, timeout?: number): Chromeless<T>
  wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless<T>
  wait(firstArg, ...args: any[]): Chromeless<T> {
    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<T> {
    this.queue.enqueue({ type: 'clearCache' })

    return this
  }

  clearStorage(origin: string, storageTypes: string): Chromeless<T> {
    this.queue.enqueue({ type: 'clearStorage', origin, storageTypes })

    return this
  }

  focus(selector: string): Chromeless<T> {
    this.queue.enqueue({ type: 'focus', selector })
    return this
  }

  press(keyCode: number, count?: number, modifiers?: any): Chromeless<T> {
    this.queue.enqueue({ type: 'press', keyCode, count, modifiers })

    return this
  }

  type(input: string, selector?: string): Chromeless<T> {
    this.queue.enqueue({ type: 'type', input, selector })

    return this
  }

  back(): Chromeless<T> {
    throw new Error('Not implemented yet')
  }

  forward(): Chromeless<T> {
    throw new Error('Not implemented yet')
  }

  refresh(): Chromeless<T> {
    throw new Error('Not implemented yet')
  }

  mousedown(selector: string): Chromeless<T> {
    this.queue.enqueue({ type: 'mousedown', selector })
    return this
  }

  mouseup(selector: string): Chromeless<T> {
    this.queue.enqueue({ type: 'mouseup', selector })
    return this
  }

  mouseover(): Chromeless<T> {
    throw new Error('Not implemented yet')
  }

  scrollTo(x: number, y: number): Chromeless<T> {
    this.queue.enqueue({ type: 'scrollTo', x, y })

    return this
  }

  scrollToElement(selector: string): Chromeless<T> {
    this.queue.enqueue({ type: 'scrollToElement', selector })

    return this
  }

  setViewport(options: DeviceMetrics): Chromeless<T> {
    this.queue.enqueue({ type: 'setViewport', options })

    return this
  }

  setHtml(html: string): Chromeless<T> {
    this.queue.enqueue({ type: 'setHtml', html })

    return this
  }

  setExtraHTTPHeaders(headers: Headers): Chromeless<T> {
    this.queue.enqueue({ type: 'setExtraHTTPHeaders', headers })

    return this
  }

  evaluate<U extends any>(
    fn: (...args: any[]) => U,
    ...args: any[]
  ): Chromeless<U> {
    this.lastReturnPromise = this.queue.process<U>({
      type: 'returnCode',
      fn: fn.toString(),
      args,
    })

    return new Chromeless<U>({}, this)
  }

  inputValue(selector: string): Chromeless<string> {
    this.lastReturnPromise = this.queue.process<string>({
      type: 'returnInputValue',
      selector,
    })

    return new Chromeless<string>({}, this)
  }

  exists(selector: string): Chromeless<boolean> {
    this.lastReturnPromise = this.queue.process<boolean>({
      type: 'returnExists',
      selector,
    })

    return new Chromeless<boolean>({}, this)
  }

  screenshot(
    selector?: string,
    options?: ScreenshotOptions,
  ): Chromeless<string> {
    if (typeof selector === 'object') {
      options = selector
      selector = undefined
    }
    this.lastReturnPromise = this.queue.process<string>({
      type: 'returnScreenshot',
      selector,
      options,
    })

    return new Chromeless<string>({}, this)
  }

  html(): Chromeless<string> {
    this.lastReturnPromise = this.queue.process<string>({ type: 'returnHtml' })

    return new Chromeless<string>({}, this)
  }

  htmlUrl(): Chromeless<string> {
    this.lastReturnPromise = this.queue.process<string>({
      type: 'returnHtmlUrl',
    })

    return new Chromeless<string>({}, this)
  }

  pdf(options?: PdfOptions): Chromeless<string> {
    this.lastReturnPromise = this.queue.process<string>({
      type: 'returnPdf',
      options,
    })

    return new Chromeless<string>({}, this)
  }

  /**
   * Get the cookies for the current url
   */
  cookies(): Chromeless<Cookie[] | null>
  /**
   * Get a specific cookie for the current url
   * @param name
   */
  cookies(name: string): Chromeless<Cookie | null>
  /**
   * Get a specific cookie by query. Not implemented yet
   * @param query
   */
  cookies(query: CookieQuery): Chromeless<Cookie[] | null>
  cookies(
    nameOrQuery?: string | CookieQuery,
  ): Chromeless<Cookie | Cookie[] | null> {
    if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery !== 'string') {
      throw new Error('Querying cookies is not implemented yet')
    }

    this.lastReturnPromise = this.queue.process<Cookie[] | Cookie | null>({
      type: 'cookies',
      nameOrQuery,
    })

    return new Chromeless<Cookie | Cookie[] | null>({}, this)
  }

  allCookies(): Chromeless<Cookie[]> {
    this.lastReturnPromise = this.queue.process<Cookie[]>({
      type: 'allCookies',
    })

    return new Chromeless<Cookie[]>({}, this)
  }

  setCookies(name: string, value: string): Chromeless<T>
  setCookies(cookie: Cookie): Chromeless<T>
  setCookies(cookies: Cookie[]): Chromeless<T>
  setCookies(nameOrCookies, value?: string): Chromeless<T> {
    this.queue.enqueue({ type: 'setCookies', nameOrCookies, value })

    return this
  }

  deleteCookies(name: string, url: string): Chromeless<T> {
    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<T> {
    this.queue.enqueue({ type: 'clearCookies' })

    return this
  }

  clearInput(selector: string): Chromeless<T> {
    this.queue.enqueue({ type: 'clearInput', selector })
    return this
  }

  setFileInput(selector: string, files: string): Chromeless<T>
  setFileInput(selector: string, files: string[]): Chromeless<T>
  setFileInput(selector: string, files: string | string[]): Chromeless<T> {
    if (!isArray(files)) {
      files = [files]
    }
    this.queue.enqueue({ type: 'setFileInput', selector, files })
    return this
  }

  async end(): Promise<T> {
    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<any> {
    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<void> {
    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<void> {
    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<void> {
    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<void> {
    this.userAgentValue = useragent
    await this.log(`Set useragent to ${this.userAgentValue}`)
  }

  private async waitTimeout(timeout: number): Promise<void> {
    this.log(`Waiting for ${timeout}ms`)
    await wait(timeout)
  }

  private async waitSelector(
    selector: string,
    waitTimeout: number = this.chromelessOptions.waitTimeout,
  ): Promise<void> {
    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<void> {
    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<T>(fn: string, ...args: any[]): Promise<T> {
    return (await evaluate(this.client, fn, ...args)) as T
  }

  private async scrollTo<T>(x: number, y: number): Promise<void> {
    return scrollTo(this.client, x, y)
  }

  private async scrollToElement<T>(selector: string): Promise<void> {
    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<void> {
    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<void> {
    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<void> {
    await setHtml(this.client, html)
  }

  private async focus(selector: string): Promise<void> {
    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<void> {
    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<Cookie[]> {
    return await getCookies(this.client, nameOrQuery as string | undefined)
  }

  async allCookies(): Promise<Cookie[]> {
    return await getAllCookies(this.client)
  }

  async setExtraHTTPHeaders(headers: Headers): Promise<void> {
    return await setExtraHTTPHeaders(this.client, headers)
  }

  async setCookies(
    nameOrCookies: string | Cookie | Cookie[],
    value?: string,
  ): Promise<void> {
    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<void> {
    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<void> {
    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<void> {
    this.log(`Sending keyCode ${keyCode} (modifiers: ${modifiers})`)
    await press(this.client, keyCode, count, modifiers)
  }

  async returnExists(selector: string): Promise<boolean> {
    return await nodeExists(this.client, selector)
  }

  async returnInputValue(selector: string): Promise<string> {
    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<string> {
    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<string> {
    return await html(this.client)
  }

  async returnHtmlUrl(options?: { filePath?: string }): Promise<string> {
    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<string> {
    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<void> {
    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<void> {
    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<RuntimeClient>
  private chromeInstance?: LaunchedChrome

  constructor(options: ChromelessOptions = {}) {
    this.options = options

    this.runtimeClientPromise = this.initRuntimeClient()
  }

  private async initRuntimeClient(): Promise<RuntimeClient> {
    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<Client> {
    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<Client> {
    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<T extends any>(command: Command): Promise<T> {
    const { runtime } = await this.runtimeClientPromise

    return (await runtime.run(command)) as T
  }

  async close(): Promise<void> {
    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<void>
  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<void> {
    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<T extends any>(command: Command): Promise<T> {
    // wait until lambda connection is established
    await this.connectionPromise

    if (this.options.debug) {
      console.log(`Running remotely: ${JSON.stringify(command)}`)
    }

    const promise = new Promise<T>((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<void> {
    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<void>

  constructor(chrome: Chrome) {
    this.chrome = chrome
    this.flushCount = 0
    this.commandQueue = {
      0: [],
    }
  }

  async end(): Promise<void> {
    this.lastWaitAll = this.waitAll()
    await this.lastWaitAll

    await this.chrome.close()
  }

  enqueue(command: Command): void {
    this.commandQueue[this.flushCount].push(command)
  }

  async process<T extends any>(command: Command): Promise<T> {
    // 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<T>(command)
  }

  private async waitAll(): Promise<void> {
    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<T extends any>(command: Command): Promise<T>
  close(): Promise<void>
}

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<string, string>

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<number>

export interface ShapeOutsideInfo {
  bounds: Quad
  shape: Array<any>
  marginShape: Array<any>
}

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<any> => {
  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<void> {
  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<void> {
  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<void>((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<void> {
  return new Promise<void>((resolve, reject) => setTimeout(resolve, timeout))
}

export async function waitForPromise<T>(
  promise: Promise<T>,
  waitTimeout: number,
  label?: string,
): Promise<T> {
  return new Promise<T>((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<boolean> {
  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<ClientRect> {
  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<void> {
  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<T>(
  client: Client,
  fn: string,
  ...args: any[]
): Promise<T> {
  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<void> {
  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<void> {
  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<string> {
  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<void> {
  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<void> {
  const clientRect = await getClientRect(client, selector)

  return scrollTo(client, clientRect.left, clientRect.top)
}

export async function setHtml(client: Client, html: string): Promise<void> {
  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<any> {
  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<any> {
  const { Network } = client

  const result = await Network.getAllCookies()
  return result.cookies
}

export async function setCookies(
  client: Client,
  cookies: Cookie[],
): Promise<void> {
  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<void> {
  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<void> {
  const { Network } = client

  await Network.deleteCookie({ cookieName: name, url })
}

export async function clearCookies(client: Client): Promise<void> {
  const { Network } = client

  await Network.clearBrowserCookies()
}

export async function getBoxModel(
  client: Client,
  selector: string,
): Promise<BoxModel> {
  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<string> {
  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<string> {
  const { DOM } = client

  const { root: { nodeId } } = await DOM.getDocument()
  const { outerHTML } = await DOM.getOuterHTML({ nodeId })
  return outerHTML
}

export async function htmlUrl(client: Client): Promise<string> {
  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<string> {
  const { Page } = client

  const pdf = await Page.printToPDF(options)

  return pdf.data
}

export async function clearInput(
  client: Client,
  selector: string,
): Promise<void> {
  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<string> {
  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<string> {
  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
  }
}
Download .txt
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
Download .txt
SYMBOL INDEX (160 symbols across 13 files)

FILE: examples/extract-google-results.js
  function run (line 3) | async function run() {

FILE: examples/google-pdf.js
  function run (line 3) | async function run() {

FILE: examples/google-screenshot.js
  function run (line 3) | async function run() {

FILE: examples/mouse-event-example.js
  function run (line 3) | async function run() {

FILE: examples/twitter.js
  function run (line 6) | async function run() {

FILE: serverless/src/utils.ts
  function createPresignedURL (line 8) | function createPresignedURL(
  function debug (line 42) | function debug(...log) {

FILE: src/api.ts
  class Chromeless (line 16) | class Chromeless<T extends any> implements Promise<T> {
    method constructor (line 20) | constructor(options: ChromelessOptions = {}, copyInstance?: Chromeless...
    method then (line 65) | then<U>(
    method catch (line 72) | catch<U>(onrejected?: (reason: any) => U | PromiseLike<U>): Promise<U> {
    method goto (line 76) | goto(url: string, timeout?: number): Chromeless<T> {
    method setUserAgent (line 82) | setUserAgent(useragent: string): Chromeless<T> {
    method click (line 88) | click(selector: string, x?: number, y?: number): Chromeless<T> {
    method wait (line 97) | wait(firstArg, ...args: any[]): Chromeless<T> {
    method clearCache (line 122) | clearCache(): Chromeless<T> {
    method clearStorage (line 128) | clearStorage(origin: string, storageTypes: string): Chromeless<T> {
    method focus (line 134) | focus(selector: string): Chromeless<T> {
    method press (line 139) | press(keyCode: number, count?: number, modifiers?: any): Chromeless<T> {
    method type (line 145) | type(input: string, selector?: string): Chromeless<T> {
    method back (line 151) | back(): Chromeless<T> {
    method forward (line 155) | forward(): Chromeless<T> {
    method refresh (line 159) | refresh(): Chromeless<T> {
    method mousedown (line 163) | mousedown(selector: string): Chromeless<T> {
    method mouseup (line 168) | mouseup(selector: string): Chromeless<T> {
    method mouseover (line 173) | mouseover(): Chromeless<T> {
    method scrollTo (line 177) | scrollTo(x: number, y: number): Chromeless<T> {
    method scrollToElement (line 183) | scrollToElement(selector: string): Chromeless<T> {
    method setViewport (line 189) | setViewport(options: DeviceMetrics): Chromeless<T> {
    method setHtml (line 195) | setHtml(html: string): Chromeless<T> {
    method setExtraHTTPHeaders (line 201) | setExtraHTTPHeaders(headers: Headers): Chromeless<T> {
    method evaluate (line 207) | evaluate<U extends any>(
    method inputValue (line 220) | inputValue(selector: string): Chromeless<string> {
    method exists (line 229) | exists(selector: string): Chromeless<boolean> {
    method screenshot (line 238) | screenshot(
    method html (line 255) | html(): Chromeless<string> {
    method htmlUrl (line 261) | htmlUrl(): Chromeless<string> {
    method pdf (line 269) | pdf(options?: PdfOptions): Chromeless<string> {
    method cookies (line 292) | cookies(
    method allCookies (line 307) | allCookies(): Chromeless<Cookie[]> {
    method setCookies (line 318) | setCookies(nameOrCookies, value?: string): Chromeless<T> {
    method deleteCookies (line 324) | deleteCookies(name: string, url: string): Chromeless<T> {
    method clearCookies (line 336) | clearCookies(): Chromeless<T> {
    method clearInput (line 342) | clearInput(selector: string): Chromeless<T> {
    method setFileInput (line 349) | setFileInput(selector: string, files: string | string[]): Chromeless<T> {
    method end (line 357) | async end(): Promise<T> {

FILE: src/chrome/local-runtime.ts
  class LocalRuntime (line 47) | class LocalRuntime {
    method constructor (line 52) | constructor(client: Client, chromelessOptions: ChromelessOptions) {
    method run (line 57) | async run(command: Command): Promise<any> {
    method goto (line 131) | private async goto(
    method clearCache (line 146) | private async clearCache(): Promise<void> {
    method clearStorage (line 157) | private async clearStorage(
    method setUserAgent (line 171) | private async setUserAgent(useragent: string): Promise<void> {
    method waitTimeout (line 176) | private async waitTimeout(timeout: number): Promise<void> {
    method waitSelector (line 181) | private async waitSelector(
    method click (line 190) | private async click(selector: string, x?: number, y?: number): Promise...
    method returnCode (line 213) | private async returnCode<T>(fn: string, ...args: any[]): Promise<T> {
    method scrollTo (line 217) | private async scrollTo<T>(x: number, y: number): Promise<void> {
    method scrollToElement (line 221) | private async scrollToElement<T>(selector: string): Promise<void> {
    method mousedown (line 233) | private async mousedown(selector: string): Promise<void> {
    method mouseup (line 255) | private async mouseup(selector: string): Promise<void> {
    method setHtml (line 275) | private async setHtml(html: string): Promise<void> {
    method focus (line 279) | private async focus(selector: string): Promise<void> {
    method type (line 298) | async type(text: string, selector?: string): Promise<void> {
    method cookies (line 318) | async cookies(nameOrQuery?: string | CookieQuery): Promise<Cookie[]> {
    method allCookies (line 322) | async allCookies(): Promise<Cookie[]> {
    method setExtraHTTPHeaders (line 326) | async setExtraHTTPHeaders(headers: Headers): Promise<void> {
    method setCookies (line 330) | async setCookies(
    method deleteCookies (line 355) | async deleteCookies(name: string, url: string): Promise<void> {
    method clearCookies (line 366) | async clearCookies(): Promise<void> {
    method press (line 377) | async press(keyCode: number, count?: number, modifiers?: any): Promise...
    method returnExists (line 382) | async returnExists(selector: string): Promise<boolean> {
    method returnInputValue (line 386) | async returnInputValue(selector: string): Promise<string> {
    method returnScreenshot (line 395) | async returnScreenshot(
    method returnHtml (line 426) | async returnHtml(): Promise<string> {
    method returnHtmlUrl (line 430) | async returnHtmlUrl(options?: { filePath?: string }): Promise<string> {
    method returnPdf (line 441) | async returnPdf(options?: PdfOptions): Promise<string> {
    method clearInput (line 452) | async clearInput(selector: string): Promise<void> {
    method setFileInput (line 474) | async setFileInput(selector: string, files: string[]): Promise<void> {
    method log (line 495) | private log(msg: string): void {

FILE: src/chrome/local.ts
  type RuntimeClient (line 8) | interface RuntimeClient {
  class LocalChrome (line 13) | class LocalChrome implements Chrome {
    method constructor (line 18) | constructor(options: ChromelessOptions = {}) {
    method initRuntimeClient (line 24) | private async initRuntimeClient(): Promise<RuntimeClient> {
    method startChrome (line 37) | private async startChrome(): Promise<Client> {
    method connectToChrome (line 73) | private async connectToChrome(): Promise<Client> {
    method setViewport (line 82) | private async setViewport(client: Client) {
    method process (line 121) | async process<T extends any>(command: Command): Promise<T> {
    method close (line 127) | async close(): Promise<void> {

FILE: src/chrome/remote.ts
  type RemoteResult (line 6) | interface RemoteResult {
  function getEndpoint (line 11) | function getEndpoint(remoteOptions: RemoteOptions | boolean): RemoteOpti...
  class RemoteChrome (line 31) | class RemoteChrome implements Chrome {
    method constructor (line 42) | constructor(options: ChromelessOptions) {
    method initConnection (line 47) | private async initConnection(): Promise<void> {
    method process (line 152) | async process<T extends any>(command: Command): Promise<T> {
    method close (line 183) | async close(): Promise<void> {

FILE: src/queue.ts
  class Queue (line 3) | class Queue {
    method constructor (line 11) | constructor(chrome: Chrome) {
    method end (line 19) | async end(): Promise<void> {
    method enqueue (line 26) | enqueue(command: Command): void {
    method process (line 30) | async process<T extends any>(command: Command): Promise<T> {
    method waitAll (line 48) | private async waitAll(): Promise<void> {

FILE: src/types.ts
  type Client (line 1) | interface Client {
  type DeviceMetrics (line 17) | interface DeviceMetrics {
  type ScreenOrientation (line 26) | interface ScreenOrientation {
  type RemoteOptions (line 31) | interface RemoteOptions {
  type CDPOptions (line 36) | interface CDPOptions {
  type ChromelessOptions (line 43) | interface ChromelessOptions {
  type Chrome (line 58) | interface Chrome {
  type Command (line 63) | type Command =
  type Headers (line 200) | type Headers = Record<string, string>
  type Cookie (line 202) | interface Cookie {
  type CookieQuery (line 215) | interface CookieQuery {
  type PdfOptions (line 226) | interface PdfOptions {
  type ScreenshotOptions (line 242) | interface ScreenshotOptions {
  type Quad (line 247) | type Quad = Array<number>
  type ShapeOutsideInfo (line 249) | interface ShapeOutsideInfo {
  type BoxModel (line 255) | interface BoxModel {
  type Viewport (line 265) | interface Viewport {

FILE: src/util.ts
  function setViewport (line 28) | async function setViewport(
  function waitForNode (line 66) | async function waitForNode(
  function wait (line 106) | async function wait(timeout: number): Promise<void> {
  function waitForPromise (line 110) | async function waitForPromise<T>(
  function eventToPromise (line 131) | function eventToPromise() {
  function nodeExists (line 146) | async function nodeExists(
  function getClientRect (line 164) | async function getClientRect(client, selector): Promise<ClientRect> {
  function click (line 194) | async function click(
  function focus (line 222) | async function focus(client: Client, selector: string): Promise<void> {
  function evaluate (line 232) | async function evaluate<T>(
  function type (line 272) | async function type(
  function press (line 295) | async function press(
  function getValue (line 328) | async function getValue(
  function scrollTo (line 344) | async function scrollTo(
  function scrollToElement (line 359) | async function scrollToElement(
  function setHtml (line 368) | async function setHtml(client: Client, html: string): Promise<void> {
  function getCookies (line 375) | async function getCookies(
  function getAllCookies (line 397) | async function getAllCookies(client: Client): Promise<any> {
  function setCookies (line 404) | async function setCookies(
  function setExtraHTTPHeaders (line 418) | async function setExtraHTTPHeaders(
  function mousedown (line 426) | async function mousedown(
  function mouseup (line 447) | async function mouseup(client: Client, selector: string, scale: number) {
  function getUrlFromCookie (line 464) | function getUrlFromCookie(cookie: Cookie) {
  function deleteCookie (line 469) | async function deleteCookie(
  function clearCookies (line 479) | async function clearCookies(client: Client): Promise<void> {
  function getBoxModel (line 485) | async function getBoxModel(
  function boxModelToViewPort (line 501) | function boxModelToViewPort(model: BoxModel, scale: number): Viewport {
  function screenshot (line 511) | async function screenshot(
  function html (line 538) | async function html(client: Client): Promise<string> {
  function htmlUrl (line 546) | async function htmlUrl(client: Client): Promise<string> {
  function pdf (line 554) | async function pdf(
  function clearInput (line 565) | async function clearInput(
  function setFileInput (line 606) | async function setFileInput(
  function getDebugOption (line 620) | function getDebugOption(): boolean {
  function writeToFile (line 633) | function writeToFile(
  function getS3BucketName (line 644) | function getS3BucketName() {
  function getS3BucketUrl (line 648) | function getS3BucketUrl() {
  function getS3ObjectKeyPrefix (line 652) | function getS3ObjectKeyPrefix() {
  function getS3FilesPermissions (line 656) | function getS3FilesPermissions() {
  function isS3Configured (line 660) | function isS3Configured() {
  function uploadToS3 (line 676) | async function uploadToS3(
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (152K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 1752,
    "preview": "version: 2\n\nworkflows:\n  version: 2\n  chromeless:\n    jobs:\n      - build_node_6\n      - build_node_8\n      - release:\n "
  },
  {
    "path": ".editorconfig",
    "chars": 135,
    "preview": "\n[*]\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nen"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 1520,
    "preview": "<!--\n\nThe issue tracker is only for bug reports or feature requests.\n\n1. If you have a question and not a bug/feature re"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 665,
    "preview": "<!--\n  Thanks for filing a pull request for Chromeless!\n\n  Please look at the following checklist to ensure that your PR"
  },
  {
    "path": ".gitignore",
    "chars": 99,
    "preview": "node_modules\ndist\n.idea\n*.log\n.DS_Store\n.serverless\n.build\n.envrc\n.nyc_output/\ncoverage/\nyarn.lock\n"
  },
  {
    "path": ".prettierignore",
    "chars": 67,
    "preview": ".nyc_output/\n*.md\ndist/\npackage.json\npackage-lock.json\nserverless/\n"
  },
  {
    "path": ".prettierrc",
    "chars": 69,
    "preview": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3220,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2232,
    "preview": "# Contributing to this project\n\n[fork]: https://github.com/graphcool/chromeless/fork\n[pr]: https://github.com/graphcool/"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2017 Contributors et.al.\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "README.md",
    "chars": 16322,
    "preview": "*This project is deprecated in favor for [Puppeteer](https://github.com/GoogleChrome/puppeteer). \nThanks to all the cont"
  },
  {
    "path": "docs/api.md",
    "chars": 17532,
    "preview": "# API Documentation\n\nChromeless provides TypeScript typings.\n\n### Chromeless constructor options\n\n`new Chromeless(option"
  },
  {
    "path": "examples/extract-google-results.js",
    "chars": 789,
    "preview": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless({ remote: true "
  },
  {
    "path": "examples/google-pdf.js",
    "chars": 442,
    "preview": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const pdf "
  },
  {
    "path": "examples/google-screenshot.js",
    "chars": 417,
    "preview": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const scre"
  },
  {
    "path": "examples/mocha-chai-test-example.js",
    "chars": 1391,
    "preview": "const { Chromeless } = require('chromeless')\nconst { expect } = require('chai')\n\n// make sure you do npm i chai\n// to ru"
  },
  {
    "path": "examples/mouse-event-example.js",
    "chars": 425,
    "preview": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n    const chromeless = new Chromeless()\n\n    const "
  },
  {
    "path": "examples/twitter.js",
    "chars": 491,
    "preview": "const { Chromeless } = require('chromeless')\n\nconst twitterUsername = \"xxx\"\nconst twitterPassword = \"xxx\"\n\nasync functio"
  },
  {
    "path": "package.json",
    "chars": 2181,
    "preview": "{\n  \"name\": \"chromeless\",\n  \"version\": \"1.4.0\",\n  \"description\": \"🖥 Chrome automation made simple. Runs locally or headl"
  },
  {
    "path": "serverless/README.md",
    "chars": 4237,
    "preview": "# Chromeless Proxy service\n\nA [Serverless](https://serverless.com/) AWS Lambda service for running and interacting with "
  },
  {
    "path": "serverless/package.json",
    "chars": 704,
    "preview": "{\n  \"name\": \"chromeless-remotechrome-service\",\n  \"version\": \"1.3.0\",\n  \"description\": \"The Chromeless Proxy AWS Lambda s"
  },
  {
    "path": "serverless/serverless.yml",
    "chars": 2685,
    "preview": "service: chromeless-serverless\n\ncustom:\n  stage: dev\n  debug: \"*\"\n  awsIotHost: ${env:AWS_IOT_HOST}\n  chrome:\n    functi"
  },
  {
    "path": "serverless/src/disconnect.ts",
    "chars": 438,
    "preview": "import * as AWS from 'aws-sdk'\nimport { debug } from './utils'\n\nconst iotData = new AWS.IotData({ endpoint: process.env."
  },
  {
    "path": "serverless/src/run.ts",
    "chars": 4542,
    "preview": "import 'source-map-support/register'\nimport { LocalChrome, Queue, ChromelessOptions } from 'chromeless'\nimport { connect"
  },
  {
    "path": "serverless/src/session.ts",
    "chars": 357,
    "preview": "import { connect as mqtt, MqttClient } from 'mqtt'\nimport * as cuid from 'cuid'\nimport { createPresignedURL, debug } fro"
  },
  {
    "path": "serverless/src/utils.ts",
    "chars": 1332,
    "preview": "import * as aws4 from 'aws4'\n\n/*\n  This creates a presigned URL for accessing the AWS IoT MQTT Broker.\n  Notably, the se"
  },
  {
    "path": "serverless/src/version.ts",
    "chars": 375,
    "preview": "import { version as chromelessVersion } from 'chromeless'\n\nconst serverlessChromelessVersion = require('../package.json'"
  },
  {
    "path": "serverless/tsconfig.json",
    "chars": 191,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"rootDir\": \".\",\n    \"target\": \"es6\",\n    \"sourceMap\": true,\n    \""
  },
  {
    "path": "src/__tests__/test.html",
    "chars": 20305,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n</head>\n<body>\n<div class=\"c"
  },
  {
    "path": "src/api.ts",
    "chars": 8933,
    "preview": "import ChromeLocal from './chrome/local'\nimport ChromeRemote from './chrome/remote'\nimport Queue from './queue'\nimport {"
  },
  {
    "path": "src/chrome/local-runtime.ts",
    "chars": 14591,
    "preview": "import {\n  Client,\n  Command,\n  ChromelessOptions,\n  Headers,\n  Cookie,\n  CookieQuery,\n  PdfOptions,\n  ScreenshotOptions"
  },
  {
    "path": "src/chrome/local.ts",
    "chars": 4350,
    "preview": "import { Chrome, Command, ChromelessOptions, Client } from '../types'\nimport * as CDP from 'chrome-remote-interface'\nimp"
  },
  {
    "path": "src/chrome/remote.ts",
    "chars": 5641,
    "preview": "import { Chrome, ChromelessOptions, Command, RemoteOptions } from '../types'\nimport { connect as mqtt, MqttClient } from"
  },
  {
    "path": "src/index.ts",
    "chars": 291,
    "preview": "import Chromeless from './api'\nimport Queue from './queue'\nimport LocalChrome from './chrome/local'\nimport { version } f"
  },
  {
    "path": "src/queue.ts",
    "chars": 1453,
    "preview": "import { Chrome, Command } from './types'\n\nexport default class Queue {\n  private flushCount: number\n  private commandQu"
  },
  {
    "path": "src/types.ts",
    "chars": 4657,
    "preview": "export interface Client {\n  Network: any\n  Page: any\n  DOM: any\n  Input: any\n  Runtime: any\n  Emulation: any\n  Storage: "
  },
  {
    "path": "src/util.test.ts",
    "chars": 1722,
    "preview": "import * as fs from 'fs'\nimport * as os from 'os'\nimport * as CDP from 'chrome-remote-interface'\nimport test from 'ava'\n"
  },
  {
    "path": "src/util.ts",
    "chars": 16154,
    "preview": "import * as fs from 'fs'\nimport * as os from 'os'\nimport * as path from 'path'\nimport * as cuid from 'cuid'\nimport {\n  C"
  },
  {
    "path": "tsconfig.json",
    "chars": 410,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es5\",\n    \"moduleResolution\": \"node\",\n    \"sourceMap\":"
  },
  {
    "path": "tslint.json",
    "chars": 231,
    "preview": "{\n  \"rules\": {\n    \"class-name\": true,\n    \"comment-format\": [true, \"check-space\"],\n    \"no-var-keyword\": true,\n    \"no-"
  }
]

About this extraction

This page contains the full source code of the schickling/chromeless GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (141.0 KB), approximately 45.2k tokens, and a symbol index with 160 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!