[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\n\nworkflows:\n  version: 2\n  chromeless:\n    jobs:\n      - build_node_6\n      - build_node_8\n      - release:\n          requires:\n            - build_node_6\n            - build_node_8\n          filters:\n            branches:\n              only: master\n\nrestore_cache: &restore_cache\n  restore_cache:\n    keys:\n      - npm-cache-{{ checksum \"package-lock.json\" }}\n\nsave_cache: &save_cache\n  save_cache:\n    key: npm-cache-{{ checksum \"package-lock.json\" }}\n    paths:\n      - ~/.npm\n\ncodecov: &codecov\n  run:\n    name: Codecov\n    command: node_modules/.bin/nyc report --reporter=json && bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json\n\njobs:\n  # Build, lint, run tests\n  build_node_6:\n    working_directory: ~/chromeless\n    docker:\n      - image: circleci/node:6\n      - image: yukinying/chrome-headless-browser\n    steps:\n      - run: node -v; npm -v\n      - checkout\n      - *restore_cache\n      - run: npm install\n      - *save_cache\n      - run: npm test\n      - *codecov\n  build_node_8:\n    working_directory: ~/chromeless\n    docker:\n      - image: circleci/node:8\n      - image: yukinying/chrome-headless-browser\n    steps:\n      - run: node -v; npm -v\n      - checkout\n      - *restore_cache\n      - run: npm install\n      - *save_cache\n      - run: npm test\n      - *codecov\n\n  # On master and if tests passed, parse the commit history to see if a new release should happen\n  # If yes, publish to npm and tag a release on GitHub\n  release:\n    working_directory: ~/chromeless\n    docker:\n      - image: circleci/node:8\n    steps:\n      - run: node -v; npm -v\n      - checkout\n      - *restore_cache\n      - run: npm install\n      - *save_cache\n      - run: npm run build\n      - run: npm run semantic-release\n"
  },
  {
    "path": ".editorconfig",
    "content": "\n[*]\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nend_of_line = lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\n\nThe issue tracker is only for bug reports or feature requests.\n\n1. If you have a question and not a bug/feature request please ask it on StackOverflow here: https://stackoverflow.com/questions/ask?tags=chromeless\n    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.\n2. Please search for and check if an issue already exists so there are no duplicates\n3. Check out and follow our Guidelines: https://github.com/graphcool/chromeless/blob/master/CONTRIBUTING.md\n4. Fill out the whole template so we have a good overview on the issue\n5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue\n6. Please follow the template, otherwise we'll have to ask you to update it\n-->\n\n# This is a (Bug Report / Feature Proposal)\n\n## Description\n\nFor bug reports:\n* What went wrong?\n* What did you expect should have happened?\n* What was the config you used?\n* What stacktrace or error message from your provider did you see?\n\nFor feature proposals:\n* What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us.\n* If there is a new API method, how would it look\n\nSimilar or dependent issues:\n* #12345\n\n## Additional Data\n\n* ***Chromeless Version you're using***:\n* ***Operating System***:\n* ***Stack Trace***:\n* ***Error messages***:\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n  Thanks for filing a pull request for Chromeless!\n\n  Please look at the following checklist to ensure that your PR\n  can be accepted quickly. Once all the items are checked-off (and CircleCI is passing), we will review your PR:\n-->\n\n- [ ] If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes)\n- [ ] Make sure all of the significant new logic is covered by tests\n- [ ] Rebase your changes on master so that they can be merged easily\n- [ ] Make sure all tests and linter rules pass\n- [ ] If you've changed APIs, update the documentation in [README](/) and [/api/README](/api/README.md)\n"
  },
  {
    "path": ".gitignore",
    "content": "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",
    "content": ".nyc_output/\n*.md\ndist/\npackage.json\npackage-lock.json\nserverless/\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn 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.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject 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.\n\nProject 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.\n\n## Scope\n\nThis 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.\n\n## Enforcement\n\nInstances 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.\n\nProject 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.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to this project\n\n[fork]: https://github.com/graphcool/chromeless/fork\n[pr]: https://github.com/graphcool/chromeless/compare\n[code-of-conduct]: CODE_OF_CONDUCT.md\n\nHi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.\n\nPlease 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.\n\n## Contribution Agreement\n\nAs 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.\n\n\n## Submitting a pull request\n\n0. [Fork][fork] and clone the repository\n0. Create a new branch: `git checkout -b feature/my-new-feature-name`\n0. Run `npm install` to make sure you've got the latest dependencies.\n0. Make your change\n0. Run the unit tests and make sure they pass and have 100% coverage. (`npm test`)\n0. Push to your fork and [submit a pull request][pr]\n0. Pat your self on the back and wait for your pull request to be reviewed and merged.\n\nHere are a few things you can do that will increase the likelihood of your pull request being accepted:\n\n- 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.\n- Make your commit message follow [Conventional Commits](https://conventionalcommits.org/).\n- 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.\n- Make sure that all the unit tests still pass. PRs with failing tests won't be merged.\n\n## Resources\n\n- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)\n- [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/)\n- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)\n- [GitHub Help](https://help.github.com)\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Contributors et.al.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "*This project is deprecated in favor for [Puppeteer](https://github.com/GoogleChrome/puppeteer). \nThanks to all the contributors who made this project possible.*\n\n# Chromeless\n\n[![npm](https://img.shields.io/npm/v/chromeless.svg)](https://npmjs.com/package/chromeless)\n[![downloads](https://img.shields.io/npm/dm/chromeless.svg)](https://npmjs.com/package/chromeless)\n[![circleci](https://circleci.com/gh/prismagraphql/chromeless.svg?style=shield)](https://circleci.com/gh/prismagraphql/workflows/chromeless/tree/master)\n[![codecov](https://codecov.io/gh/prismagraphql/chromeless/branch/master/graph/badge.svg)](https://codecov.io/gh/prismagraphql/chromeless)\n[![dependencies](https://david-dm.org/prismagraphql/chromeless/status.svg)](https://david-dm.org/prismagraphql/chromeless)\n[![node](https://img.shields.io/node/v/chromeless.svg)]()\n[![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)\n\nChrome automation made simple. Runs locally or headless on AWS Lambda. (**[See Demo](https://chromeless.netlify.com/)**)\n\n## Chromeless can be used to...\n\n* Run 1000s of **browser integration tests in parallel** ⚡️\n* Crawl the web & automate screenshots\n* Write bots that require a real browser\n* *Do pretty much everything you've used __PhantomJS, NightmareJS or Selenium__ for before*\n\n### Examples\n\n* [JSON of Google Results](examples/extract-google-results.js): Google for `chromeless` and get a list of JSON results\n* [Screenshot of Google Results](examples/google-screenshot.js): Google for `chromeless` and take a screenshot of the results\n* [prep](https://github.com/prismagraphql/prep): Compile-time prerendering for SPA/PWA (like React, Vue...) instead of server-side rendering (SSR)\n* *See the full [examples list](/examples) for more*\n\n## ▶️ Try it out\n\nYou 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)).\n\n[![](http://i.imgur.com/i1gtCzy.png)](https://chromeless.netlify.com/)\n\n## Contents\n1. [How it works](#how-it-works)\n1. [Installation](#installation)\n1. [Usage](#usage)\n1. [API Documentation](#api-documentation)\n1. [Configuring Development Environment](#configuring-development-environment)\n1. [FAQ](#faq)\n1. [Contributors](#contributors)\n1. [Credits](#credits)\n1. [Help & Community](#help-and-community)\n\n## How it works\n\nWith 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.\n\n### There are 2 ways to use Chromeless\n\n1. Running Chrome on your local computer\n2. Running Chrome on AWS Lambda and controlling it remotely\n\n![](http://imgur.com/2bgTyAi.png)\n\n### 1. Local Setup\n\nFor 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.\n\n### 2. Remote Proxy Setup\n\nYou 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.)\n\nChromeless 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.\n\n## Installation\n```sh\nnpm install chromeless\n```\n\n### Proxy Setup\n\nThe project contains a [Serverless](https://serverless.com/) service for running and driving Chrome remotely on AWS Lambda.\n\n1. Deploy The Proxy service to AWS Lambda. More details [here](serverless#setup)\n2. Follow the usage instructions [here](serverless#using-the-proxy).\n\n\n## Usage\n\nUsing Chromeless is similar to other browser automation tools. For example:\n\n```js\nconst { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const screenshot = await chromeless\n    .goto('https://www.google.com')\n    .type('chromeless', 'input[name=\"q\"]')\n    .press(13)\n    .wait('#resultStats')\n    .screenshot()\n\n  console.log(screenshot) // prints local file path or S3 url\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n```\n\n### Local Chrome Usage\n\nTo 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.\n\nTo launch Chrome yourself, and open the port for chromeless, follow this example:\n\n```sh\nalias canary=\"/Applications/Google\\ Chrome\\ Canary.app/Contents/MacOS/Google\\ Chrome\\ Canary\"\ncanary --remote-debugging-port=9222\n```\n\nOr run Chrome Canary headless-ly:\n\n```sh\ncanary --remote-debugging-port=9222 --disable-gpu --headless\n```\n\nOr run Chrome headless-ly on Windows:\n\n```sh\ncd \"C:\\Program Files (x86)\\Google\\Chrome\\Application\"\nchrome --remote-debugging-port=9222 --disable-gpu --headless\n```\n\n### Proxy Usage\n\nFollow the setup instructions [here](serverless#installation).\n\nThen using Chromeless with the Proxy service is the same as running it locally with the exception of the `remote` option.\nAlternatively you can configure the Proxy service's endpoint with environment variables. [Here's how](serverless#using-the-proxy).\n```js\nconst chromeless = new Chromeless({\n  remote: {\n    endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',\n    apiKey: 'your-api-key-here',\n  },\n})\n```\n\n## API Documentation\n\n**Chromeless constructor options**\n- [`new Chromeless(options: ChromelessOptions)`](docs/api.md#chromeless-constructor-options)\n\n**Chromeless methods**\n- [`end()`](docs/api.md#api-end)\n\n**Chrome methods**\n- [`goto(url: string, timeout?: number)`](docs/api.md#api-goto)\n- [`setUserAgent(useragent: string)`](docs/api.md#api-setUserAgent)\n- [`click(selector: string, x?: number, y?: number)`](docs/api.md#api-click)\n- [`wait(timeout: number)`](docs/api.md#api-wait-timeout)\n- [`wait(selector: string)`](docs/api.md#api-wait-selector)\n- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet\n- [`clearCache()`](docs/api.md#api-clearcache)\n- [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage)\n- [`focus(selector: string)`](docs/api.md#api-focus)\n- [`press(keyCode: number, count?: number, modifiers?: any)`](docs/api.md#api-press)\n- [`type(input: string, selector?: string)`](docs/api.md#api-type)\n- [`back()`](docs/api.md#api-back) - Not implemented yet\n- [`forward()`](docs/api.md#api-forward) - Not implemented yet\n- [`refresh()`](docs/api.md#api-refresh) - Not implemented yet\n- [`mousedown(selector: string)`](docs/api.md#api-mousedown)\n- [`mouseup(selector: string)`](docs/api.md#api-mouseup)\n- [`scrollTo(x: number, y: number)`](docs/api.md#api-scrollto)\n- [`scrollToElement(selector: string)`](docs/api.md#api-scrolltoelement)\n- [`setHtml(html: string)`](docs/api.md#api-sethtml)\n- [`setExtraHTTPHeaders(headers: Headers)`](docs/api.md#api-setextrahttpheaders)\n- [`setViewport(options: DeviceMetrics)`](docs/api.md#api-setviewport)\n- [`evaluate<U extends any>(fn: (...args: any[]) => void, ...args: any[])`](docs/api.md#api-evaluate)\n- [`inputValue(selector: string)`](docs/api.md#api-inputvalue)\n- [`exists(selector: string)`](docs/api.md#api-exists)\n- [`screenshot(selector: string, options: ScreenshotOptions)`](docs/api.md#api-screenshot)\n- [`pdf(options?: PdfOptions)`](docs/api.md#api-pdf)\n- [`html()`](docs/api.md#api-html)\n- [`cookies()`](docs/api.md#api-cookies)\n- [`cookies(name: string)`](docs/api.md#api-cookies-name)\n- [`cookies(query: CookieQuery)`](docs/api.md#api-cookies-query) - Not implemented yet\n- [`allCookies()`](docs/api.md#api-all-cookies)\n- [`setCookies(name: string, value: string)`](docs/api.md#api-setcookies)\n- [`setCookies(cookie: Cookie)`](docs/api.md#api-setcookies-one)\n- [`setCookies(cookies: Cookie[])`](docs/api.md#api-setcookies-many)\n- [`deleteCookies(name: string)`](docs/api.md#api-deletecookies)\n- [`clearCookies()`](docs/api.md#api-clearcookies)\n- [`clearInput(selector: string)`](docs/api.md#api-clearInput)\n- [`setFileInput(selector: string, files: string | string[])`](docs/api.md#api-set-file-input)\n\n## Configuring Development Environment\n\n**Requirements:**\n- NodeJS version 8.2 and greater\n\n1) Clone this repository\n2) Run `npm install`\n3) To build: `npm run build`\n\n#### Linking this NPM repository\n\n1) Go to this repository locally\n2) Run `npm link`\n3) Go to the folder housing your chromeless scripts\n4) Run `npm link chromeless`\n\nNow your local chromeless scripts will use your local development of chromeless.\n\n## FAQ\n\n### How is this different from [NightmareJS](https://github.com/segmentio/nightmare), PhantomJS or Selenium?\n\nThe `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.\n\n### I'm new to AWS Lambda, is this still for me?\n\nYou 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`.\n\n### How much does it cost to run Chromeless in production?\n\n> 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.\n\nThis means you can easily execute > 100.000 tests for free in the free tier.\n\n### Are there any limitations?\n\nIf 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)\n\n### Are there commercial options?\n\nAlthough 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:\n\n* [Chromatic](http://chromaticqa.com): Visual snapshot regression testing for [Storybook](https://storybook.js.org/).\n\n## Troubleshooting\n### Error: Unable to get presigned websocket URL and connect to it.\nIn case you get an error like this when running the Chromeless client:\n```\n{ HTTPError: Response code 403 (Forbidden)\n    at stream.catch.then.data (/code/chromeless/node_modules/got/index.js:182:13)\n    at process._tickDomainCallback (internal/process/next_tick.js:129:7)\n  name: 'HTTPError',\n...\nError: Unable to get presigned websocket URL and connect to it.\n```\nMake 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`.\n\n### Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev\nIn case the deployment of the serverless function returns an error like this:\n```\n  Serverless Error ---------------------------------------\n\n  Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev\n```\nPlease check, that there is no stack with the name `chromeless-serverless-dev` existing yet, otherwise serverless can't correctly provision the bucket.\n\n### No command gets executed\nIn 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.\n\n## Contributors\n\nA big thank you to all contributors and supporters of this repository 💚\n\n<a href=\"https://github.com/joelgriffith/\" target=\"_blank\">\n  <img src=\"https://github.com/joelgriffith.png?size=64\" width=\"64\" height=\"64\" alt=\"joelgriffith\">\n</a>\n<a href=\"https://github.com/adieuadieu/\" target=\"_blank\">\n  <img src=\"https://github.com/adieuadieu.png?size=64\" width=\"64\" height=\"64\" alt=\"adieuadieu\">\n</a>\n<a href=\"https://github.com/schickling/\" target=\"_blank\">\n  <img src=\"https://github.com/schickling.png?size=64\" width=\"64\" height=\"64\" alt=\"schickling\">\n</a>\n<a href=\"https://github.com/timsuchanek/\" target=\"_blank\">\n  <img src=\"https://github.com/timsuchanek.png?size=64\" width=\"64\" height=\"64\" alt=\"timsuchanek\">\n</a>\n\n\n<a href=\"https://github.com/Chrisgozd/\" target=\"_blank\">\n  <img src=\"https://github.com/Chrisgozd.png?size=64\" width=\"64\" height=\"64\" alt=\"Chrisgozd\">\n</a>\n<a href=\"https://github.com/criticalbh/\" target=\"_blank\">\n  <img src=\"https://github.com/criticalbh.png?size=64\" width=\"64\" height=\"64\" alt=\"criticalbh\">\n</a>\n<a href=\"https://github.com/d2s/\" target=\"_blank\">\n  <img src=\"https://github.com/d2s.png?size=64\" width=\"64\" height=\"64\" alt=\"d2s\">\n</a>\n<a href=\"https://github.com/emeth-/\" target=\"_blank\">\n  <img src=\"https://github.com/emeth-.png?size=64\" width=\"64\" height=\"64\" alt=\"emeth-\">\n</a>\n<a href=\"https://github.com/githubixx/\" target=\"_blank\">\n  <img src=\"https://github.com/githubixx.png?size=64\" width=\"64\" height=\"64\" alt=\"githubixx\">\n</a>\n<a href=\"https://github.com/hax/\" target=\"_blank\">\n  <img src=\"https://github.com/hax.png?size=64\" width=\"64\" height=\"64\" alt=\"hax\">\n</a>\n<a href=\"https://github.com/Hazealign/\" target=\"_blank\">\n  <img src=\"https://github.com/Hazealign.png?size=64\" width=\"64\" height=\"64\" alt=\"Hazealign\">\n</a>\n<a href=\"https://github.com/joeyvandijk/\" target=\"_blank\">\n  <img src=\"https://github.com/joeyvandijk.png?size=64\" width=\"64\" height=\"64\" alt=\"joeyvandijk\">\n</a>\n<a href=\"https://github.com/liady/\" target=\"_blank\">\n  <img src=\"https://github.com/liady.png?size=64\" width=\"64\" height=\"64\" alt=\"liady\">\n</a>\n<a href=\"https://github.com/matthewmueller/\" target=\"_blank\">\n  <img src=\"https://github.com/matthewmueller.png?size=64\" width=\"64\" height=\"64\" alt=\"matthewmueller\">\n</a>\n<a href=\"https://github.com/seangransee/\" target=\"_blank\">\n  <img src=\"https://github.com/seangransee.png?size=64\" width=\"64\" height=\"64\" alt=\"seangransee\">\n</a>\n<a href=\"https://github.com/sorenbs/\" target=\"_blank\">\n  <img src=\"https://github.com/sorenbs.png?size=64\" width=\"64\" height=\"64\" alt=\"sorenbs\">\n</a>\n<a href=\"https://github.com/toddwprice/\" target=\"_blank\">\n  <img src=\"https://github.com/toddwprice.png?size=64\" width=\"64\" height=\"64\" alt=\"toddwprice\">\n</a>\n<a href=\"https://github.com/vladgolubev/\" target=\"_blank\">\n  <img src=\"https://github.com/vladgolubev.png?size=64\" width=\"64\" height=\"64\" alt=\"vladgolubev\">\n</a>\n\n\n\n\n## Credits\n\n* [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface): Chromeless uses this package as an interface to Chrome\n* [serverless-chrome](https://github.com/adieuadieu/serverless-chrome): Compiled Chrome binary that runs on AWS Lambda (Azure and GCP soon, too.)\n* [NightmareJS](https://github.com/segmentio/nightmare): We draw a lot of inspiration for the API from this great tool\n\n\n<a name=\"help-and-community\" />\n\n## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool)\n\nJoin our [Slack community](http://slack.graph.cool/) if you run into issues or have questions. We love talking to you!\n\n<p align=\"center\"><a href=\"https://oss.prisma.io\"><img src=\"https://imgur.com/IMU2ERq.png\" alt=\"Prisma\" height=\"170px\"></a></p>\n"
  },
  {
    "path": "docs/api.md",
    "content": "# API Documentation\n\nChromeless provides TypeScript typings.\n\n### Chromeless constructor options\n\n`new Chromeless(options: ChromelessOptions)`\n\n- `debug: boolean` Show debug output — Default: `false`\n- `remote: boolean` Use remote chrome process — Default: `false`\n- `implicitWait: boolean` Wait for element to exist before executing commands — Default: `false`\n- `waitTimeout: number` Time in ms to wait for element to appear — Default: `10000`\n- `scrollBeforeClick: boolean` Scroll to element before clicking, usefull if element is outside of viewport — Default: `false`\n- `viewport: any` Viewport dimensions — Default: `{width: 1440, height: 900, scale: 1}`\n- `launchChrome: boolean` Auto-launch chrome (local) — Default: `true`\n- `cdp: CDPOptions` Chome Debugging Protocol Options — Default: `{host: 'localhost', port: 9222, secure: false, closeTab: true}`\n\n### Chromeless methods\n- [`end()`](#api-end)\n\n### Chrome methods\n- [`goto(url: string, timeout?: number)`](#api-goto)\n- [`setUserAgent(useragent: string)`](#api-setuseragent)\n- [`click(selector: string, x?: number, y?: number)`](#api-click)\n- [`wait(timeout: number)`](#api-wait-timeout)\n- [`wait(selector: string, timeout?: number)`](#api-wait-selector)\n- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet\n- [`clearCache()`](#api-clearcache)\n- [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage)\n- [`focus(selector: string)`](#api-focus)\n- [`press(keyCode: number, count?: number, modifiers?: any)`](#api-press)\n- [`type(input: string, selector?: string)`](#api-type)\n- [`back()`](#api-back) - Not implemented yet\n- [`forward()`](#api-forward) - Not implemented yet\n- [`refresh()`](#api-refresh) - Not implemented yet\n- [`mousedown(selector: string)`](#api-mousedown)\n- [`mouseup(selector: string)`](#api-mouseup)\n- [`scrollTo(x: number, y: number)`](#api-scrollto)\n- [`scrollToElement(selector: string)`](#api-scrolltoelement)\n- [`setHtml(html: string)`](#api-sethtml)\n- [`setViewport(options: DeviceMetrics)`](#api-setviewport)\n- [`evaluate<U extends any>(fn: (...args: any[]) => void, ...args: any[])`](#api-evaluate)\n- [`inputValue(selector: string)`](#api-inputvalue)\n- [`exists(selector: string)`](#api-exists)\n- [`screenshot(selector: string, options: ScreenshotOptions)`](#api-screenshot)\n- [`pdf(options?: PdfOptions)`](#api-pdf)\n- [`html()`](#api-html)\n- [`cookies()`](#api-cookies)\n- [`cookies(name: string)`](#api-cookies-name)\n- [`cookies(query: CookieQuery)`](#api-cookies-query) - Not implemented yet\n- [`allCookies()`](#api-all-cookies)\n- [`setCookies(name: string, value: string)`](#api-setcookies)\n- [`setCookies(cookie: Cookie)`](#api-setcookies-one)\n- [`setCookies(cookies: Cookie[])`](#api-setcookies-many)\n- [`deleteCookies(name: string)`](#api-deletecookies)\n- [`clearCookies()`](#api-clearcookies)\n\n\n---------------------------------------\n\n<a name=\"api-end\" />\n\n### end(): Promise<T>\n\nEnd the Chromeless session. Locally this will disconnect from Chrome. Over the Proxy, this will end the session, terminating the Lambda function.\nIt returns the last value that has been evaluated.\n\n```js\nawait chromeless.end()\n```\n\n---------------------------------------\n\n<a name=\"api-goto\" />\n\n### goto(url: string, timeout?: number): Chromeless<T>\n\nNavigate to a URL.\n\n__Arguments__\n- `url` - URL to navigate to\n- `timeout` -How long to wait for page to load (default is value of waitTimeout option)\n\n__Example__\n\n```js\nawait chromeless.goto('https://google.com/')\n```\n\n---------------------------------------\n\n<a name=\"api-setuseragent\" />\n\n### setUserAgent(useragent: string): Chromeless<T>\n\nSet the useragent of the browser. It should be called before `.goto()`.\n\n__Arguments__\n- `useragent` - UserAgent to use\n\n__Example__\n\n```js\nawait chromeless.setUserAgent('Custom Chromeless UserAgent x.x.x')\n```\n\n---------------------------------------\n\n<a name=\"api-click\" />\n\n### click(selector: string, x?: number, y?: number): Chromeless<T>\n\nClick on something in the DOM.\n\n__Arguments__\n- `selector` - DOM selector for element to click\n- `x` - Offset from the left of the element, default width/2\n- `y` - Offset from the top of the element, default height/2\n\n__Example__\n\n```js\nawait chromeless.click('#button')\nawait chromeless.click('#button', 20, 100)\n```\n\n---------------------------------------\n\n<a name=\"api-wait-timeout\" />\n\n### wait(timeout: number): Chromeless<T>\n\nWait for some duration. Useful for waiting for things download.\n\n__Arguments__\n- `timeout` - How long to wait, in ms\n\n__Example__\n\n```js\nawait chromeless.wait(1000)\n```\n\n---------------------------------------\n\n<a name=\"api-wait-selector\" />\n\n### wait(selector: string, timeout?: number): Chromeless<T>\n\nWait until something appears. Useful for waiting for things to render.\n\n__Arguments__\n- `selector` - DOM selector to wait for\n- `timeout` - How long to wait for element to appear (default is value of waitTimeout option)\n\n__Example__\n\n```js\nawait chromeless.wait('div#loaded')\nawait chromeless.wait('div#loaded', 1000)\n```\n\n---------------------------------------\n\n<a name=\"api-wait-fn\" />\n\n### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless<T>\n\nNot implemented yet\n\nWait until a function returns. You can also return some Promise that will be resolved at some point.\n\n__Arguments__\n- `fn` - Function to wait for\n- `[arguments]` - Arguments to pass to the function\n\n__Example__\n\n```js\nawait chromeless.wait(() => {\n  return new Promise((resolve, reject) => {\n    // do something async, setTimeout...\n    resolve();\n  });\n})\n```\n\n---------------------------------------\n\n<a name=\"api-clearcache\" />\n\n### clearCache(): Chromeless<T>\n\nClears browser cache.\n\nService 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).\n\n__Example__\n\n```js\nawait chromeless.clearCache()\n```\n\n---------------------------------------\n\n<a name=\"api-clearstorage\" />\n\n### clearStorage(origin: string, storageTypes: string): Chromeless<T>\n\nClears browser storage.\n\n__Arguments__\n- `origin` - Security origin for the storage type we wish to clear\n\n- `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/).\n\n__Example__\n\n```js\nawait chromeless.clearStorage('http://localhost', 'local_storage, websql')\n\nawait chromeless.clearStorage('*', 'all')\n```\n\n---------------------------------------\n\n<a name=\"api-focus\" />\n\n### focus(selector: string): Chromeless<T>\n\nProvide focus on a DOM element.\n\n__Arguments__\n- `selector` - DOM selector to focus\n\n__Example__\n\n```js\nawait chromeless.focus('input#searchField')\n```\n\n---------------------------------------\n\n<a name=\"api-press\" />\n\n### press(keyCode: number, count?: number, modifiers?: any): Chromeless<T>\n\nSend a key press. Enter, for example.\n\n__Arguments__\n- `keyCode` - Key code to send\n- `count` - How many times to send the key press\n- `modifiers` - Modifiers to send along with the press (e.g. control, command, or alt)\n\n__Example__\n\n```js\nawait chromeless.press(13)\n```\n\n---------------------------------------\n\n<a name=\"api-type\" />\n\n### type(input: string, selector?: string): Chromeless<T>\n\nType something (into a field, for example).\n\n__Arguments__\n- `input` - String to type\n- `selector` - DOM element to type into\n\n__Example__\n\n```js\nconst result = await chromeless\n  .goto('https://www.google.com')\n  .type('chromeless', 'input[name=\"q\"]')\n```\n\n---------------------------------------\n\n<a name=\"api-back\" />\n\n### back() - Not implemented yet\n\nNot implemented yet\n\n---------------------------------------\n\n<a name=\"api-forward\" />\n\n### forward() - Not implemented yet\n\nNot implemented yet\n\n---------------------------------------\n\n<a name=\"api-refresh\" />\n\n### refresh() - Not implemented yet\n\nNot implemented yet\n\n---------------------------------------\n\n<a name=\"api-mousedown\" />\n\n### mousedown(selector: string): Chromeless<T>\n\nSend mousedown event on something in the DOM.\n\n__Arguments__\n- `selector` - DOM selector for element to send mousedown event\n\n__Example__\n\n```js\nawait chromeless.mousedown('#item')\n```\n\n---------------------------------------\n\n<a name=\"api-mouseup\" />\n\n### mouseup(selector: string): Chromeless<T>\n\nSend mouseup event on something in the DOM.\n\n__Arguments__\n- `selector` - DOM selector for element to send mouseup event\n\n__Example__\n\n```js\nawait chromeless.mouseup('#placeholder')\n```\n\n---------------------------------------\n\n<a name=\"api-scrollto\" />\n\n### scrollTo(x: number, y: number): Chromeless<T>\n\nScroll to somewhere in the document.\n\n__Arguments__\n- `x` - Offset from the left of the document\n- `y` - Offset from the top of the document\n\n__Example__\n\n```js\nawait chromeless.scrollTo(0, 500)\n```\n\n---------------------------------------\n\n<a name=\"api-scrolltoelement\" />\n\n### scrollToElement(selector: string): Chromeless<T>\n\nScroll to location of element. Behavior is simiar to `<a href=\"#fragment\"></a>` — target element will be at the top of viewport\n\n__Arguments__\n- `selector` - DOM selector for element to scroll to\n\n__Example__\n\n  ```js\nawait chromeless.scrollToElement('.button')\n  ```\n\n  ---------------------------------------\n\n<a name=\"api-sethtml\" />\n\n### setHtml(html: string): Chromeless<T>\n\nSets given markup as the document's HTML.\n\n__Arguments__\n- `html` - HTML to set as the document's markup.\n\n__Example__\n\n```js\nawait chromeless.setHtml('<h1>Hello world!</h1>')\n```\n\n  ---------------------------------------\n\n<a name=\"api-setextrahttpheaders\" />\n\n### setExtraHTTPHeaders(headers: Headers): Chromeless<T>\n\nSets extra HTTP headers.\n\n__Arguments__\n- `headers` - headers as keys / values of JSON object\n\n__Example__\n\n```js\nawait chromeless.setExtraHTTPHeaders({\n  'accept-language': 'en-US,en;q=0.8'\n})\n```\n\n\n---------------------------------------\n\n<a name=\"api-setviewport\" />\n\n### setViewport(options:DeviceMetrics)\n\nResize the viewport. Useful if you want to capture more or less of the document in a screenshot.\n\n__Arguments__\n- `options` - DeviceMetrics object\n\n__Example__\n\n```js\nawait chromeless.setViewport({width: 1024, height: 600, scale: 1})\n```\n\n---------------------------------------\n\n<a name=\"api-evaluate\" />\n\n### evaluate<U extends any>(fn: (...args: any[]) => void, ...args: any[]): Chromeless<U>\n\nEvaluate Javascript code within Chrome in the context of the DOM. Returns the resulting value or a Promise.\n\n__Arguments__\n- `fn` - Function to evaluate within Chrome, can be async (Promise).\n- `[arguments]` - Arguments to pass to the function\n\n__Example__\n\n```js\nawait chromeless.evaluate(() => {\n    // this will be executed in Chrome\n    const links = [].map.call(\n      document.querySelectorAll('.g h3 a'),\n      a => ({title: a.innerText, href: a.href})\n    )\n    return JSON.stringify(links)\n  })\n```\n\n---------------------------------------\n\n<a name=\"api-inputvalue\" />\n\n### inputValue(selector: string): Chromeless<string>\n\nGet the value of an input field.\n\n__Arguments__\n- `selector` - DOM input element\n\n__Example__\n\n```js\nawait chromeless.inputValue('input#searchField')\n```\n\n---------------------------------------\n\n<a name=\"api-exists\" />\n\n### exists(selector: string): Chromeless<boolean>\n\nTest if a DOM element exists in the document.\n\n__Arguments__\n- `selector` - DOM element to check for\n\n__Example__\n\n```js\nawait chromeless.exists('div#ready')\n```\n\n---------------------------------------\n\n<a name=\"api-screenshot\" />\n\n### screenshot(selector: string, options: ScreenshotOptions): Chromeless<string>\n\nTake a screenshot of the document as framed by the viewport or of a specific element (by a selector).\nWhen running Chromeless locally this returns the local file path to the screenshot image.\nWhen run over the Chromeless Proxy service, a URL to the screenshot on S3 is returned.\n\n__Arguments__\n- `selector` - DOM element to take a screenshot of,\n- `options` - An options object with the following props\n- `options.filePath` - A file path override in case of working locally\n- `options.omitBackground` - Boolean to remove default white background\n\n__Examples__\n\n```js\nconst screenshot = await chromeless\n  .goto('https://google.com/')\n  .screenshot()\n\nconsole.log(screenshot) // prints local file path or S3 URL\n```\n\n```js\nconst screenshot = await chromeless\n  .goto('https://google.com/')\n  .screenshot('#hplogo', { filePath: path.join(__dirname, 'google-logo.png') })\n\nconsole.log(screenshot) // prints local file path or S3 URL\n```\n\n```js\nconst screenshot = await chromeless\n  .goto('https://google.com/')\n  .screenshot({ filePath: path.join(__dirname, 'google-search.png') })\n\nconsole.log(screenshot) // prints local file path or S3 URL\n```\n\n---------------------------------------\n\n<a name=\"api-pdf\" />\n\n### pdf(options?: PdfOptions) - Chromeless<string>\n\nPrint to a PDF of the document as framed by the viewport.\nWhen running Chromeless locally this returns the local file path to the PDF.\nWhen run over the Chromeless Proxy service, a URL to the PDF on S3 is returned.\n\nRequires that Chrome be running headless-ly. [More](https://github.com/graphcool/chromeless/issues/146)\n\n\n__Arguments__\n- `options` - An object containing overrides for [printToPDF() parameters](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF)\n\n__Example__\n\n```js\nconst pdf = await chromeless\n  .goto('https://google.com/')\n  .pdf({landscape: true})\n\nconsole.log(pdf) // prints local file path or S3 URL\n```\n\n---------------------------------------\n\n<a name=\"api-html\" />\n\n### html(): Chromeless<string>\n\nGet full HTML of the loaded page.\n\n__Example__\n\n```js\nconst html = await chromeless\n  .setHtml('<h1>Hello world!</h1>')\n  .html()\n\nconsole.log(html) // <html><head></head><body><h1>Hello world!</h1></body></html>\n```\n\n---------------------------------------\n\n<a name=\"api-cookies\" />\n\n### cookies(): Chromeless<Cookie[] | null>\n\nReturns all browser cookies for the current URL.\n\n__Example__\n\n```js\nawait chromeless.cookies()\n```\n\n---------------------------------------\n\n<a name=\"api-cookies-name\" />\n\n### cookies(name: string): Chromeless<Cookie | null>\n\nReturns a specific browser cookie by name for the current URL.\n\n__Arguments__\n- `name` - Name of the cookie to get\n\n__Example__\n\n```js\nconst cookie = await chromeless.cookies('creepyTrackingCookie')\n```\n\n---------------------------------------\n\n<a name=\"api-cookies-query\" />\n\n### cookies(query: CookieQuery) - Not implemented yet\n\nNot implemented yet\n\n---------------------------------------\n\n<a name=\"api-all-cookies\" />\n\n### allCookies(): Chromeless<Cookie[]>\n\nReturns all browser cookies. Nam nom nom.\n\n__Example__\n\n```js\nawait chromeless.allCookies()\n```\n\n---------------------------------------\n\n<a name=\"api-setcookies\" />\n\n### setCookies(name: string, value: string): Chromeless<T>\n\nSets a cookie with the given name and value.\n\n__Arguments__\n- `name` - Name of the cookie\n- `value` - Value of the cookie\n\n__Example__\n\n```js\nawait chromeless.setCookies('visited', '1')\n```\n\n---------------------------------------\n\n<a name=\"api-setcookies-one\" />\n\n### setCookies(cookie: Cookie): Chromeless<T>\n\nSets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.\n\n__Arguments__\n- `cookie` - The cookie data to set\n\n__Example__\n\n```js\nawait chromeless.setCookies({\n  url: 'http://google.com/',\n  domain: 'google.com',\n  name: 'userData',\n  value: '{}',\n  path: '/',\n  expires: 0,\n  size: 0,\n  httpOnly: false,\n  secure: true,\n  session: true,\n})\n```\n\n---------------------------------------\n\n<a name=\"api-setcookies-many\" />\n\n### setCookies(cookies: Cookie[]): Chromeless<T>\n\nSets many cookies with the given cookie data; may overwrite equivalent cookies if they exist.\n\n__Arguments__\n- `url` - URL to navigate to\n\n__Example__\n\n```js\nawait chromeless.setCookies([\n  {\n    url: 'http://google.com/',\n    domain: 'google.com',\n    name: 'userData',\n    value: '{}',\n    path: '/',\n    expires: 0,\n    size: 0,\n    httpOnly: false,\n    secure: true,\n    session: true,\n  }, {\n    url: 'http://bing.com/',\n    domain: 'bing.com',\n    name: 'userData',\n    value: '{}',\n    path: '/',\n    expires: 0,\n    size: 0,\n    httpOnly: false,\n    secure: true,\n    session: true,\n  }\n])\n```\n\n---------------------------------------\n\n<a name=\"api-deletecookies\" />\n\n### deleteCookies(name: string) - Not implemented yet\n\nDelete a specific cookie.\n\n__Arguments__\n- `name` - name of the cookie\n\n__Example__\n\n```js\nawait chromeless.deleteCookies('cookieName')\n```\n\n---------------------------------------\n\n<a name=\"api-clearcookies\" />\n\n### clearCookies(): Chromeless<T>\n\nClears all browser cookies.\n\n__Example__\n\n```js\nawait chromeless.clearCookies()\n```\n---------------------------------------\n\n<a name=\"api-clearInput\" />\n\n### clearInput(selector: string): Chromeless<T>\n\nClear input text.\n\n\n__Example__\n\n```js\nawait chromeless.clearInput('#username')\n```\n---------------------------------------\n\n<a name=\"api-set-file-input\" />\n\n### setFileInput(selector: string, files: string | string[]): Chromeless<T>\n\nSet file(s) for selected file input.\n\nCurrently not supported in the Proxy. Progress tracked in [#186](https://github.com/graphcool/chromeless/issues/186)\n\n\n__Example__\n\n```js\nawait chromeless.setFileInput('.uploader', '/User/Me/Documents/img.jpg')\n```\n"
  },
  {
    "path": "examples/extract-google-results.js",
    "content": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless({ remote: true })\n\n  const links = await chromeless\n    .goto('https://www.google.com')\n    .type('chromeless', 'input[name=\"q\"]')\n    .press(13)\n    .wait('#resultStats')\n    .evaluate(() => {\n      // this will be executed in headless chrome\n      const links = [].map.call(\n        document.querySelectorAll('.g h3 a'),\n        a => ({title: a.innerText, href: a.href})\n      )\n      return JSON.stringify(links)\n    })\n    // you can still use the method chaining API after evaluating\n    // when you're done, at any time you can call `.then` (in our case `await`)\n    .scrollTo(0, 1000)\n\n  console.log(links)\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n"
  },
  {
    "path": "examples/google-pdf.js",
    "content": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const pdf = await chromeless\n    .goto('https://www.google.com')\n    .type('chromeless', 'input[name=\"q\"]')\n    .press(13)\n    .wait('#resultStats')\n    .pdf({ displayHeaderFooter: true, landscape: true })\n\n  console.log(pdf) // prints local file path or S3 url\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n"
  },
  {
    "path": "examples/google-screenshot.js",
    "content": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const screenshot = await chromeless\n    .goto('https://www.google.com')\n    .type('chromeless', 'input[name=\"q\"]')\n    .press(13)\n    .wait('#resultStats')\n    .screenshot()\n\n  console.log(screenshot) // prints local file path or S3 url\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n"
  },
  {
    "path": "examples/mocha-chai-test-example.js",
    "content": "const { Chromeless } = require('chromeless')\nconst { expect } = require('chai')\n\n// make sure you do npm i chai\n// to run this example just run\n// mocha path/to/this/file\n\ndescribe('When searching on google', function () {\n  it('shows results', async function () {\n    this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error\n    const chromeless = new Chromeless()\n\n    await chromeless.goto('https://google.com')\n      .wait('input[name=\"q\"]')\n      .type('chromeless github', 'input[name=\"q\"]')\n      .press(13) // press enter\n      .wait('#resultStats')\n\n\n    const result = await chromeless.exists('a[href*=\"graphcool/chromeless\"]')\n\n\n    expect(result).to.be.true\n    await chromeless.end()\n  })\n})\n\ndescribe('When clicking on the image of the demo playground', function () {\n  it('should redirect to the demo', async function () {\n    this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error\n    const chromeless = new Chromeless()\n    await chromeless.goto('https://github.com/graphcool/chromeless')\n      .wait('a[href=\"https://chromeless.netlify.com/\"]')\n      .click('a[href=\"https://chromeless.netlify.com/\"]')\n      .wait('#root')\n\n\n    const url = await chromeless.evaluate(url => window.location.href)\n\n\n    expect(url).to.match(/^https\\:\\/\\/chromeless\\.netlify\\.com/)\n    await chromeless.end()\n  })\n})\n"
  },
  {
    "path": "examples/mouse-event-example.js",
    "content": "const { Chromeless } = require('chromeless')\n\nasync function run() {\n    const chromeless = new Chromeless()\n\n    const screenshot = await chromeless\n        .goto('https://www.google.com')\n        .mousedown('input[name=\"btnI\"]')\n        .mouseup('input[name=\"btnI\"]')\n        .wait('.latest-doodle')\n        .screenshot()\n\n    console.log(screenshot)\n\n    await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n"
  },
  {
    "path": "examples/twitter.js",
    "content": "const { Chromeless } = require('chromeless')\n\nconst twitterUsername = \"xxx\"\nconst twitterPassword = \"xxx\"\n\nasync function run() {\n  const chromeless = new Chromeless()\n\n  const screenshot = await chromeless\n    .goto('https://twitter.com/login/')\n    .type(twitterUsername, '.js-username-field')\n    .type(twitterPassword, '.js-password-field')\n    .click('button[type=\"submit\"]')\n    .wait('.status')\n    .screenshot()\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chromeless\",\n  \"version\": \"1.4.0\",\n  \"description\": \"🖥 Chrome automation made simple. Runs locally or headless on AWS Lambda.\",\n  \"homepage\": \"https://github.com/graphcool/chromeless\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/graphcool/chromeless.git\"\n  },\n  \"bug\": {\n    \"url\": \"https://github.com/graphcool/chromeless/issues\"\n  },\n  \"main\": \"dist/src/index.js\",\n  \"typings\": \"dist/src/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">= 6.10.0\"\n  },\n  \"scripts\": {\n    \"ava\": \"tsc -d && nyc ava\",\n    \"build\": \"npm run clean && tsc -d\",\n    \"clean\": \"rimraf dist\",\n    \"coverage\": \"npm run ava\",\n    \"precommit\": \"lint-staged\",\n    \"commitmsg\": \"commitlint -e $GIT_PARAMS\",\n    \"prettier\": \"prettier --list-different --write \\\"**/*.{ts,json}\\\"\",\n    \"test\": \"npm run lint && npm run ava\",\n    \"lint\": \"npm run prettier && npm run tslint\",\n    \"tslint\": \"tslint -c tslint.json -p tsconfig.json --exclude 'node_modules/**'\",\n    \"watch\": \"tsc -w\",\n    \"watch:test\": \"tsc -d -w & ava --watch\",\n    \"semantic-release\": \"semantic-release\"\n  },\n  \"dependencies\": {\n    \"aws-sdk\": \"^2.177.0\",\n    \"bluebird\": \"^3.5.1\",\n    \"chrome-launcher\": \"^0.10.0\",\n    \"chrome-remote-interface\": \"^0.25.5\",\n    \"cuid\": \"^2.1.0\",\n    \"form-data\": \"^2.3.1\",\n    \"got\": \"^8.0.0\",\n    \"mqtt\": \"^2.15.0\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/config-conventional\": \"^7.0.0\",\n    \"@types/bluebird\": \"^3.5.19\",\n    \"@types/cuid\": \"^1.3.0\",\n    \"@types/node\": \"^10.0.3\",\n    \"ava\": \"^0.25.0\",\n    \"commitlint\": \"^7.0.0\",\n    \"husky\": \"^0.14.3\",\n    \"lint-staged\": \"^7.0.0\",\n    \"nyc\": \"^12.0.2\",\n    \"prettier\": \"1.11.1\",\n    \"rimraf\": \"^2.6.2\",\n    \"semantic-release\": \"^15.0.2\",\n    \"tslint\": \"^5.8.0\",\n    \"typescript\": \"^2.6.2\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"lint-staged\": {\n    \"*.{ts}\": [\n      \"prettier --parser typescript --no-semi --single-quote --trailing-comma all --write\",\n      \"tslint\",\n      \"git add\"\n    ],\n    \"*.{js}\": [\n      \"prettier --no-semi --single-quote --trailing-comma all --write\",\n      \"lint\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "serverless/README.md",
    "content": "# Chromeless Proxy service\n\nA [Serverless](https://serverless.com/) AWS Lambda service for running and interacting with Chrome remotely with Chromeless.\n\n\n## Contents\n1. [Setup](#setup)\n1. [Using the Proxy](#using-the-proxy)\n\n\n## Setup\n\nClone this repository and enter the `serverless` directory:\n\n```bash\ngit clone https://github.com/graphcool/chromeless.git\ncd chromeless/serverless\nnpm install\n```\n\n### Configure\n\nNext, modify the `custom` section in `serverless.yml`.\n\nYou 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.\n\nFor example:\n\n```yaml\n...\n\ncustom:\n  stage: dev\n  debug: \"*\" # false if you don't want noise in CloudWatch\n  awsIotHost: ${env:AWS_IOT_HOST}\n\n...\n```\n\nYou may also need to change the region in the `provider` section in `serverless.yml`:\n\n\n```yaml\n...\n\nprovider:\n  name: aws\n  runtime: nodejs6.10\n  stage: ${self:custom.stage}\n  region: YOUR_REGION_HERE\n\n...\n```\n\n**Note:** The AWS Lambda function, API Gateway and IoT must all be in the _same_ region.\n\n**Note:** Deploying from Windows is currently not supported. See [#70](https://github.com/graphcool/chromeless/issues/70#issuecomment-318634457)\n\n\n### Credentials\n\nBefore 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/).\n\nIn short, either:\n\n```bash\nexport AWS_PROFILE=<your-profile-name>\n```\n\nor\n\n```bash\nexport AWS_ACCESS_KEY_ID=<your-key-here>\nexport AWS_SECRET_ACCESS_KEY=<your-secret-key-here>\n```\n\n### Deploy\n\nOnce configured, deploying the service can be done with:\n\n```bash\nnpm run deploy\n```\n\nOnce 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.\n\n```log\nService Information\nservice: chromeless-serverless\nstage: dev\nregion: eu-west-1\napi keys:\n  dev-chromeless-session-key: X-your-api-key-here-X\nendpoints:\n  GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/version\n  OPTIONS - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/\n  GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/\nfunctions:\n  run: chromeless-serverless-dev-run\n  version: chromeless-serverless-dev-version\n  session: chromeless-serverless-dev-session\n  disconnect: chromeless-serverless-dev-disconnect\n```\n\n\n## Using the Proxy\n\nConnect 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/`\n\n\n### Option 1: Environment Variables\n\n```bash\nexport CHROMELESS_ENDPOINT_URL=https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev\nexport CHROMELESS_ENDPOINT_API_KEY=your-api-key-here\n```\nand\n```js\nconst chromeless = new Chromeless({\n  remote: true,\n})\n```\n\n### Option 2: Constructor options\n\n```js\nconst chromeless = new Chromeless({\n  remote: {\n    endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',\n    apiKey: 'your-api-key-here',\n  },\n})\n```\n\n\n### Full Example\n\n```js\nconst Chromeless = require('chromeless').default\n\nasync function run() {\n  const chromeless = new Chromeless({\n    remote: {\n      endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev',\n      apiKey: 'your-api-key-here'\n    },\n  })\n\n  const screenshot = await chromeless\n    .goto('https://www.google.com')\n    .type('chromeless', 'input[name=\"q\"]')\n    .press(13)\n    .wait('#resultStats')\n    .screenshot()\n\n  console.log(screenshot) // prints local file path or S3 url\n\n  await chromeless.end()\n}\n\nrun().catch(console.error.bind(console))\n```\n"
  },
  {
    "path": "serverless/package.json",
    "content": "{\n  \"name\": \"chromeless-remotechrome-service\",\n  \"version\": \"1.3.0\",\n  \"description\": \"The Chromeless Proxy AWS Lambda service\",\n  \"homepage\": \"https://github.com/graphcool/chromeless\",\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"node\": \">= 6.10.0\"\n  },\n  \"scripts\": {\n    \"deploy\": \"serverless deploy\"\n  },\n  \"dependencies\": {\n    \"aws4\": \"^1.6.0\",\n    \"chromeless\": \"^1.3.0\",\n    \"cuid\": \"^1.3.8\",\n    \"mqtt\": \"^2.11.0\",\n    \"source-map-support\": \"^0.4.15\"\n  },\n  \"devDependencies\": {\n    \"@types/cuid\": \"^1.3.0\",\n    \"@types/node\": \"^8.0.15\",\n    \"serverless\": \"^1.19.0\",\n    \"serverless-offline\": \"^3.15.3\",\n    \"serverless-plugin-chrome\": \"^1.0.0-11\",\n    \"serverless-plugin-typescript\": \"^1.0.0\"\n  }\n}\n"
  },
  {
    "path": "serverless/serverless.yml",
    "content": "service: chromeless-serverless\n\ncustom:\n  stage: dev\n  debug: \"*\"\n  awsIotHost: ${env:AWS_IOT_HOST}\n  chrome:\n    functions:\n      - run\n\nprovider:\n  name: aws\n  runtime: nodejs6.10\n  stage: ${self:custom.stage}\n  region: eu-west-1\n  environment:\n    DEBUG: ${self:custom.debug}\n    AWS_IOT_HOST: ${self:custom.awsIotHost}\n  apiKeys:\n    - ${self:custom.stage}-chromeless-session-key\n  iamRoleStatements:\n    - Effect: \"Allow\"\n      Action:\n        - \"iot:Connect\"\n        - \"iot:Publish\"\n        - \"iot:Subscribe\"\n        - \"iot:Receive\"\n        - \"iot:GetThingShadow\"\n        - \"iot:UpdateThingShadow\"\n      Resource: \"*\"\n    - Effect: \"Allow\"\n      Action:\n        - s3:*\n      Resource:\n        Fn::Join:\n          - \"\"\n          - - \"arn:aws:s3:::\"\n            - Ref: AWS::AccountId\n            - \"-\"\n            - Ref: AWS::Region\n            - -chromeless\n            - /*\n\nplugins:\n  - serverless-plugin-typescript\n  - serverless-plugin-chrome\n  - serverless-offline\n\nfunctions:\n  run:\n    memorySize: 1536\n    timeout: 300\n    handler: src/run.default\n    events:\n      - iot:\n          sql: \"SELECT * FROM 'chrome/new-session'\"\n    environment:\n      CHROMELESS_S3_BUCKET_NAME:\n        Fn::Join:\n          - \"\"\n          - - Ref: AWS::AccountId\n            - \"-\"\n            - Ref: AWS::Region\n            - -chromeless\n      CHROMELESS_S3_OBJECT_KEY_PREFIX: \"\"\n      CHROMELESS_S3_OBJECT_ACL: \"public-read\"\n      CHROMELESS_S3_BUCKET_URL:\n        Fn::GetAtt:\n          - Bucket\n          - DomainName\n  version:\n    memorySize: 128\n    handler: src/version.default\n    events:\n    - http:\n        path: /version\n        method: GET\n  session:\n    memorySize: 128\n    timeout: 10\n    handler: src/session.default\n    events:\n      - http:\n          method: OPTIONS\n          path: /\n          private: true\n      - http:\n          method: GET\n          path: /\n          private: true\n  disconnect:\n    memorySize: 256\n    handler: src/disconnect.default\n    timeout: 10\n    events:\n      - iot:\n          sql: \"SELECT * FROM 'chrome/last-will'\"\n\nresources:\n  Resources:\n    RunLogGroup:\n      Properties:\n        RetentionInDays: 7\n    VersionLogGroup:\n      Properties:\n        RetentionInDays: 7\n    SessionLogGroup:\n      Properties:\n        RetentionInDays: 7\n    DisconnectLogGroup:\n      Properties:\n        RetentionInDays: 7\n    Bucket:\n      Type: AWS::S3::Bucket\n      Properties:\n        BucketName:\n          Fn::Join:\n            - \"\"\n            - - Ref: AWS::AccountId\n              - \"-\"\n              - Ref: AWS::Region\n              - -chromeless\n        LifecycleConfiguration:\n          Rules:\n          - ExpirationInDays: 1\n            Status: Enabled\n"
  },
  {
    "path": "serverless/src/disconnect.ts",
    "content": "import * as AWS from 'aws-sdk'\nimport { debug } from './utils'\n\nconst iotData = new AWS.IotData({ endpoint: process.env.AWS_IOT_HOST })\n\nexport default async ({ channelId }, context, callback): Promise<void> => {\n  debug('Disconnect on', channelId)\n\n  let params = {\n    topic: `chrome/${channelId}/end`,\n    payload: JSON.stringify({ channelId, client: true, disconnected: true }),\n    qos: 1,\n  }\n\n  iotData.publish(params, callback)\n}\n"
  },
  {
    "path": "serverless/src/run.ts",
    "content": "import 'source-map-support/register'\nimport { LocalChrome, Queue, ChromelessOptions } from 'chromeless'\nimport { connect as mqtt, MqttClient } from 'mqtt'\nimport { createPresignedURL, debug } from './utils'\n\nexport default async (\n  { channelId, options },\n  context,\n  callback,\n  chromeInstance\n): Promise<void> => {\n  // used to block requests from being processed while we're exiting\n  let endingInvocation = false\n  let timeout\n  let executionCheckInterval\n\n  debug('Invoked with data: ', channelId, options)\n\n  const chrome = new LocalChrome({\n    ...options,\n    remote: false,\n    launchChrome: false,\n    cdp: { closeTab: true },\n  })\n\n  const queue = new Queue(chrome)\n\n  const TOPIC_CONNECTED = `chrome/${channelId}/connected`\n  const TOPIC_REQUEST = `chrome/${channelId}/request`\n  const TOPIC_RESPONSE = `chrome/${channelId}/response`\n  const TOPIC_END = `chrome/${channelId}/end`\n\n  const channel = mqtt(createPresignedURL())\n\n  if (process.env.DEBUG) {\n    channel.on('error', error => debug('WebSocket error', error))\n    channel.on('offline', () => debug('WebSocket offline'))\n  }\n\n  /*\n    Clean up function whenever we want to end the invocation.\n    Importantly we publish a message that we're disconnecting, and then\n    we kill the running Chrome instance.\n  */\n  const end = (topic_end_data = {}) => {\n    if (!endingInvocation) {\n      endingInvocation = true\n      clearInterval(executionCheckInterval)\n      clearTimeout(timeout)\n\n      channel.unsubscribe(TOPIC_END, () => {\n        channel.publish(TOPIC_END, JSON.stringify({ channelId, chrome: true, ...topic_end_data }), {\n          qos: 0,\n        }, async () => {\n          channel.end()\n\n          await chrome.close()\n          await chromeInstance.kill()\n\n          callback()\n        })\n      })\n    }\n  }\n\n  const newTimeout = () =>\n    setTimeout(async () => {\n      debug('Timing out. No requests received for 30 seconds.')\n      await end({ inactivity: true })\n    }, 30000)\n\n  /*\n    When we're almost out of time, we clean up.\n    Importantly this makes sure that Chrome isn't running on the next invocation\n    and publishes a message to the client letting it know we're disconnecting.\n  */\n  executionCheckInterval = setInterval(async () => {\n    if (context.getRemainingTimeInMillis() < 5000) {\n      debug('Ran out of execution time.')\n      await end({ outOfTime: true })\n    }\n  }, 1000)\n\n  channel.on('connect', () => {\n    debug('Connected to AWS IoT broker')\n\n    /*\n      Publish that we've connected. This lets the client know that\n      it can start sending requests (commands) for us to process.\n    */\n    channel.publish(TOPIC_CONNECTED, JSON.stringify({}), { qos: 1 })\n\n    /*\n      The main bit. Listen for requests from the client, handle them\n      and respond with the result.\n    */\n    channel.subscribe(TOPIC_REQUEST, () => {\n      debug(`Subscribed to ${TOPIC_REQUEST}`)\n\n      timeout = newTimeout()\n\n      channel.on('message', async (topic, buffer) => {\n        if (TOPIC_REQUEST === topic && !endingInvocation) {\n          const message = buffer.toString()\n\n          clearTimeout(timeout)\n\n          debug(`Message from ${TOPIC_REQUEST}`, message)\n\n          const command = JSON.parse(message)\n\n          try {\n            const result = await queue.process(command)\n            const remoteResult = JSON.stringify({\n              value: result,\n            })\n\n            debug('Chrome result', result)\n\n            channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 })\n          } catch (error) {\n            const remoteResult = JSON.stringify({\n              error: error.toString(),\n            })\n\n            debug('Chrome error', error)\n\n            channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 })\n          }\n\n          timeout = newTimeout()\n        }\n      })\n    })\n\n    /*\n      Handle diconnection from the client.\n      Either the client purposfully ended the session, or the client\n      connection was abruptly ended resulting in a last-will message\n      being dispatched by the IoT MQTT broker.\n      */\n    channel.subscribe(TOPIC_END, async () => {\n      channel.on('message', async (topic, buffer) => {\n        if (TOPIC_END === topic) {\n          const message = buffer.toString()\n          const data = JSON.parse(message)\n\n          debug(`Message from ${TOPIC_END}`, message)\n          debug(\n            `Client ${data.disconnected ? 'disconnected' : 'ended session'}.`\n          )\n\n          await end()\n\n          debug('Ended successfully.')\n        }\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "serverless/src/session.ts",
    "content": "import { connect as mqtt, MqttClient } from 'mqtt'\nimport * as cuid from 'cuid'\nimport { createPresignedURL, debug } from './utils'\n\nexport default async (event, context, callback): Promise<void> => {\n  const url = createPresignedURL()\n  const channelId = cuid()\n\n  callback(null, {\n    statusCode: 200,\n    body: JSON.stringify({ url, channelId }),\n  })\n}\n"
  },
  {
    "path": "serverless/src/utils.ts",
    "content": "import * as aws4 from 'aws4'\n\n/*\n  This creates a presigned URL for accessing the AWS IoT MQTT Broker.\n  Notably, the sessionToken is simply tacked on to the end, and not signed.\n  Because AWS. Thank you @shortjared for your help pointing this out.\n*/\nexport function createPresignedURL(\n  {\n    host = process.env.AWS_IOT_HOST,\n    path = '/mqtt',\n    region = process.env.AWS_REGION,\n    service = 'iotdevicegateway',\n    accessKeyId = process.env.AWS_ACCESS_KEY_ID,\n    secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY,\n    sessionToken = process.env.AWS_SESSION_TOKEN,\n    // expires = 0, // @TODO: 300, check if this is working http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html\n  } = {},\n): string {\n  const signed = aws4.sign(\n    {\n      host,\n      path,\n      service,\n      region,\n      signQuery: true,\n      // headers: {\n      //   'X-Amz-Expires': expires,\n      // },\n    },\n    {\n      accessKeyId,\n      secretAccessKey,\n    },\n  )\n\n  return `wss://${host}${signed.path}&X-Amz-Security-Token=${encodeURIComponent(\n    sessionToken,\n  )}`\n}\n\nexport function debug(...log) {\n  if (process.env.DEBUG) {\n    console.log(\n      ...log.map(\n        argument =>\n          typeof argument === 'object'\n            ? JSON.stringify(argument, null, 2)\n            : argument\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "serverless/src/version.ts",
    "content": "import { version as chromelessVersion } from 'chromeless'\n\nconst serverlessChromelessVersion = require('../package.json').version\n\nexport default async (event, context, callback): Promise<void> => {\n  callback(null, {\n    statusCode: 200,\n    body: JSON.stringify({\n      chromeless: chromelessVersion,\n      serverlessChromeless: serverlessChromelessVersion,\n    }),\n  })\n}\n"
  },
  {
    "path": "serverless/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"rootDir\": \".\",\n    \"target\": \"es6\",\n    \"sourceMap\": true,\n    \"moduleResolution\": \"node\"\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "src/__tests__/test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n</head>\n<body>\n<div class=\"container\">\n    <div class=\"logo\"><img src=\"data:image/png;base64,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\" alt=\"Chromeless\" /></div>\n    <div>\n        <p>This is a test page for Chromeless unit tests</p>\n    </div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "src/api.ts",
    "content": "import ChromeLocal from './chrome/local'\nimport ChromeRemote from './chrome/remote'\nimport Queue from './queue'\nimport {\n  ChromelessOptions,\n  Headers,\n  Cookie,\n  CookieQuery,\n  PdfOptions,\n  DeviceMetrics,\n  ScreenshotOptions,\n} from './types'\nimport { getDebugOption } from './util'\nimport { isArray } from 'util'\n\nexport default class Chromeless<T extends any> implements Promise<T> {\n  private queue: Queue\n  private lastReturnPromise: Promise<any>\n\n  constructor(options: ChromelessOptions = {}, copyInstance?: Chromeless<any>) {\n    if (copyInstance) {\n      this.queue = copyInstance.queue\n      this.lastReturnPromise = copyInstance.lastReturnPromise\n      return\n    }\n\n    const mergedOptions: ChromelessOptions = {\n      debug: getDebugOption(),\n      waitTimeout: 10000,\n      remote: false,\n      implicitWait: true,\n      scrollBeforeClick: false,\n      launchChrome: true,\n\n      ...options,\n\n      viewport: {\n        scale: 1,\n        ...options.viewport,\n      },\n\n      cdp: {\n        host: process.env['CHROMELESS_CHROME_HOST'] || 'localhost',\n        port: parseInt(process.env['CHROMELESS_CHROME_PORT'], 10) || 9222,\n        secure: false,\n        closeTab: true,\n        ...options.cdp,\n      },\n    }\n\n    const chrome = mergedOptions.remote\n      ? new ChromeRemote(mergedOptions)\n      : new ChromeLocal(mergedOptions)\n\n    this.queue = new Queue(chrome)\n\n    this.lastReturnPromise = Promise.resolve(undefined)\n  }\n\n  /*\n   * The following 3 members are needed to implement a Promise\n   */\n  readonly [Symbol.toStringTag]: 'Promise'\n\n  then<U>(\n    onFulfill?: ((value: T) => U | PromiseLike<U>) | null,\n    onReject?: ((error: any) => U | PromiseLike<U>) | null,\n  ): Promise<U> {\n    return this.lastReturnPromise.then(onFulfill, onReject) as Promise<U>\n  }\n\n  catch<U>(onrejected?: (reason: any) => U | PromiseLike<U>): Promise<U> {\n    return this.lastReturnPromise.catch(onrejected) as Promise<U>\n  }\n\n  goto(url: string, timeout?: number): Chromeless<T> {\n    this.queue.enqueue({ type: 'goto', url, timeout })\n\n    return this\n  }\n\n  setUserAgent(useragent: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'setUserAgent', useragent })\n\n    return this\n  }\n\n  click(selector: string, x?: number, y?: number): Chromeless<T> {\n    this.queue.enqueue({ type: 'click', selector, x, y })\n\n    return this\n  }\n\n  wait(timeout: number): Chromeless<T>\n  wait(selector: string, timeout?: number): Chromeless<T>\n  wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless<T>\n  wait(firstArg, ...args: any[]): Chromeless<T> {\n    switch (typeof firstArg) {\n      case 'number': {\n        this.queue.enqueue({ type: 'wait', timeout: firstArg })\n        break\n      }\n      case 'string': {\n        this.queue.enqueue({\n          type: 'wait',\n          selector: firstArg,\n          timeout: args[0],\n        })\n        break\n      }\n      case 'function': {\n        this.queue.enqueue({ type: 'wait', fn: firstArg, args })\n        break\n      }\n      default:\n        throw new Error(`Invalid wait arguments: ${firstArg} ${args}`)\n    }\n\n    return this\n  }\n\n  clearCache(): Chromeless<T> {\n    this.queue.enqueue({ type: 'clearCache' })\n\n    return this\n  }\n\n  clearStorage(origin: string, storageTypes: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'clearStorage', origin, storageTypes })\n\n    return this\n  }\n\n  focus(selector: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'focus', selector })\n    return this\n  }\n\n  press(keyCode: number, count?: number, modifiers?: any): Chromeless<T> {\n    this.queue.enqueue({ type: 'press', keyCode, count, modifiers })\n\n    return this\n  }\n\n  type(input: string, selector?: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'type', input, selector })\n\n    return this\n  }\n\n  back(): Chromeless<T> {\n    throw new Error('Not implemented yet')\n  }\n\n  forward(): Chromeless<T> {\n    throw new Error('Not implemented yet')\n  }\n\n  refresh(): Chromeless<T> {\n    throw new Error('Not implemented yet')\n  }\n\n  mousedown(selector: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'mousedown', selector })\n    return this\n  }\n\n  mouseup(selector: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'mouseup', selector })\n    return this\n  }\n\n  mouseover(): Chromeless<T> {\n    throw new Error('Not implemented yet')\n  }\n\n  scrollTo(x: number, y: number): Chromeless<T> {\n    this.queue.enqueue({ type: 'scrollTo', x, y })\n\n    return this\n  }\n\n  scrollToElement(selector: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'scrollToElement', selector })\n\n    return this\n  }\n\n  setViewport(options: DeviceMetrics): Chromeless<T> {\n    this.queue.enqueue({ type: 'setViewport', options })\n\n    return this\n  }\n\n  setHtml(html: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'setHtml', html })\n\n    return this\n  }\n\n  setExtraHTTPHeaders(headers: Headers): Chromeless<T> {\n    this.queue.enqueue({ type: 'setExtraHTTPHeaders', headers })\n\n    return this\n  }\n\n  evaluate<U extends any>(\n    fn: (...args: any[]) => U,\n    ...args: any[]\n  ): Chromeless<U> {\n    this.lastReturnPromise = this.queue.process<U>({\n      type: 'returnCode',\n      fn: fn.toString(),\n      args,\n    })\n\n    return new Chromeless<U>({}, this)\n  }\n\n  inputValue(selector: string): Chromeless<string> {\n    this.lastReturnPromise = this.queue.process<string>({\n      type: 'returnInputValue',\n      selector,\n    })\n\n    return new Chromeless<string>({}, this)\n  }\n\n  exists(selector: string): Chromeless<boolean> {\n    this.lastReturnPromise = this.queue.process<boolean>({\n      type: 'returnExists',\n      selector,\n    })\n\n    return new Chromeless<boolean>({}, this)\n  }\n\n  screenshot(\n    selector?: string,\n    options?: ScreenshotOptions,\n  ): Chromeless<string> {\n    if (typeof selector === 'object') {\n      options = selector\n      selector = undefined\n    }\n    this.lastReturnPromise = this.queue.process<string>({\n      type: 'returnScreenshot',\n      selector,\n      options,\n    })\n\n    return new Chromeless<string>({}, this)\n  }\n\n  html(): Chromeless<string> {\n    this.lastReturnPromise = this.queue.process<string>({ type: 'returnHtml' })\n\n    return new Chromeless<string>({}, this)\n  }\n\n  htmlUrl(): Chromeless<string> {\n    this.lastReturnPromise = this.queue.process<string>({\n      type: 'returnHtmlUrl',\n    })\n\n    return new Chromeless<string>({}, this)\n  }\n\n  pdf(options?: PdfOptions): Chromeless<string> {\n    this.lastReturnPromise = this.queue.process<string>({\n      type: 'returnPdf',\n      options,\n    })\n\n    return new Chromeless<string>({}, this)\n  }\n\n  /**\n   * Get the cookies for the current url\n   */\n  cookies(): Chromeless<Cookie[] | null>\n  /**\n   * Get a specific cookie for the current url\n   * @param name\n   */\n  cookies(name: string): Chromeless<Cookie | null>\n  /**\n   * Get a specific cookie by query. Not implemented yet\n   * @param query\n   */\n  cookies(query: CookieQuery): Chromeless<Cookie[] | null>\n  cookies(\n    nameOrQuery?: string | CookieQuery,\n  ): Chromeless<Cookie | Cookie[] | null> {\n    if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery !== 'string') {\n      throw new Error('Querying cookies is not implemented yet')\n    }\n\n    this.lastReturnPromise = this.queue.process<Cookie[] | Cookie | null>({\n      type: 'cookies',\n      nameOrQuery,\n    })\n\n    return new Chromeless<Cookie | Cookie[] | null>({}, this)\n  }\n\n  allCookies(): Chromeless<Cookie[]> {\n    this.lastReturnPromise = this.queue.process<Cookie[]>({\n      type: 'allCookies',\n    })\n\n    return new Chromeless<Cookie[]>({}, this)\n  }\n\n  setCookies(name: string, value: string): Chromeless<T>\n  setCookies(cookie: Cookie): Chromeless<T>\n  setCookies(cookies: Cookie[]): Chromeless<T>\n  setCookies(nameOrCookies, value?: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'setCookies', nameOrCookies, value })\n\n    return this\n  }\n\n  deleteCookies(name: string, url: string): Chromeless<T> {\n    if (typeof name === 'undefined') {\n      throw new Error('Cookie name should be defined.')\n    }\n    if (typeof url === 'undefined') {\n      throw new Error('Cookie url should be defined.')\n    }\n    this.queue.enqueue({ type: 'deleteCookies', name, url })\n\n    return this\n  }\n\n  clearCookies(): Chromeless<T> {\n    this.queue.enqueue({ type: 'clearCookies' })\n\n    return this\n  }\n\n  clearInput(selector: string): Chromeless<T> {\n    this.queue.enqueue({ type: 'clearInput', selector })\n    return this\n  }\n\n  setFileInput(selector: string, files: string): Chromeless<T>\n  setFileInput(selector: string, files: string[]): Chromeless<T>\n  setFileInput(selector: string, files: string | string[]): Chromeless<T> {\n    if (!isArray(files)) {\n      files = [files]\n    }\n    this.queue.enqueue({ type: 'setFileInput', selector, files })\n    return this\n  }\n\n  async end(): Promise<T> {\n    const result = await this.lastReturnPromise\n    await this.queue.end()\n    return result\n  }\n}\n"
  },
  {
    "path": "src/chrome/local-runtime.ts",
    "content": "import {\n  Client,\n  Command,\n  ChromelessOptions,\n  Headers,\n  Cookie,\n  CookieQuery,\n  PdfOptions,\n  ScreenshotOptions,\n} from '../types'\nimport {\n  nodeExists,\n  wait,\n  waitForNode,\n  click,\n  evaluate,\n  screenshot,\n  html,\n  htmlUrl,\n  pdf,\n  type,\n  getValue,\n  scrollTo,\n  scrollToElement,\n  setHtml,\n  setExtraHTTPHeaders,\n  press,\n  setViewport,\n  clearCookies,\n  deleteCookie,\n  getCookies,\n  setCookies,\n  getAllCookies,\n  version,\n  mousedown,\n  mouseup,\n  focus,\n  clearInput,\n  setFileInput,\n  writeToFile,\n  isS3Configured,\n  uploadToS3,\n  eventToPromise,\n  waitForPromise,\n} from '../util'\n\nexport default class LocalRuntime {\n  private client: Client\n  private chromelessOptions: ChromelessOptions\n  private userAgentValue: string\n\n  constructor(client: Client, chromelessOptions: ChromelessOptions) {\n    this.client = client\n    this.chromelessOptions = chromelessOptions\n  }\n\n  async run(command: Command): Promise<any> {\n    switch (command.type) {\n      case 'goto':\n        return this.goto(command.url, command.timeout)\n      case 'setViewport':\n        return setViewport(this.client, command.options)\n      case 'wait': {\n        if (command.selector) {\n          return this.waitSelector(command.selector, command.timeout)\n        } else if (command.timeout) {\n          return this.waitTimeout(command.timeout)\n        } else {\n          throw new Error('waitFn not yet implemented')\n        }\n      }\n      case 'clearCache':\n        return this.clearCache()\n      case 'clearStorage':\n        return this.clearStorage(command.origin, command.storageTypes)\n      case 'setUserAgent':\n        return this.setUserAgent(command.useragent)\n      case 'click':\n        return this.click(command.selector, command.x, command.y)\n      case 'returnCode':\n        return this.returnCode(command.fn, ...command.args)\n      case 'returnExists':\n        return this.returnExists(command.selector)\n      case 'returnScreenshot':\n        return this.returnScreenshot(command.selector, command.options)\n      case 'returnHtml':\n        return this.returnHtml()\n      case 'returnHtmlUrl':\n        return this.returnHtmlUrl()\n      case 'returnPdf':\n        return this.returnPdf(command.options)\n      case 'returnInputValue':\n        return this.returnInputValue(command.selector)\n      case 'type':\n        return this.type(command.input, command.selector)\n      case 'press':\n        return this.press(command.keyCode, command.count, command.modifiers)\n      case 'scrollTo':\n        return this.scrollTo(command.x, command.y)\n      case 'scrollToElement':\n        return this.scrollToElement(command.selector)\n      case 'deleteCookies':\n        return this.deleteCookies(command.name, command.url)\n      case 'clearCookies':\n        return this.clearCookies()\n      case 'setHtml':\n        return this.setHtml(command.html)\n      case 'setExtraHTTPHeaders':\n        return this.setExtraHTTPHeaders(command.headers)\n      case 'cookies':\n        return this.cookies(command.nameOrQuery)\n      case 'allCookies':\n        return this.allCookies()\n      case 'setCookies':\n        return this.setCookies(command.nameOrCookies, command.value)\n      case 'mousedown':\n        return this.mousedown(command.selector)\n      case 'mouseup':\n        return this.mouseup(command.selector)\n      case 'focus':\n        return this.focus(command.selector)\n      case 'clearInput':\n        return this.clearInput(command.selector)\n      case 'setFileInput':\n        return this.setFileInput(command.selector, command.files)\n      default:\n        throw new Error(`No such command: ${JSON.stringify(command)}`)\n    }\n  }\n\n  private async goto(\n    url: string,\n    waitTimeout: number = this.chromelessOptions.waitTimeout,\n  ): Promise<void> {\n    const { Network, Page } = this.client\n    await Promise.all([Network.enable(), Page.enable()])\n    if (!this.userAgentValue) this.userAgentValue = `Chromeless ${version}`\n    await Network.setUserAgentOverride({ userAgent: this.userAgentValue })\n    const e2p = eventToPromise()\n    Page.loadEventFired(e2p.onEvent)\n    await Page.navigate({ url })\n    await waitForPromise(e2p.fired(), waitTimeout, 'page load event')\n    this.log(`Navigated to ${url}`)\n  }\n\n  private async clearCache(): Promise<void> {\n    const { Network } = this.client\n    const canClearCache = await Network.canClearBrowserCache\n    if (canClearCache) {\n      await Network.clearBrowserCache()\n      this.log(`Cache is cleared`)\n    } else {\n      this.log(`Cache could not be cleared`)\n    }\n  }\n\n  private async clearStorage(\n    origin: string,\n    storageTypes: string,\n  ): Promise<void> {\n    const { Storage, Network } = this.client\n    const canClearCache = await Network.canClearBrowserCache\n    if (canClearCache) {\n      await Storage.clearDataForOrigin({ origin, storageTypes })\n      this.log(`${storageTypes} for ${origin} is cleared`)\n    } else {\n      this.log(`${storageTypes} could not be cleared`)\n    }\n  }\n\n  private async setUserAgent(useragent: string): Promise<void> {\n    this.userAgentValue = useragent\n    await this.log(`Set useragent to ${this.userAgentValue}`)\n  }\n\n  private async waitTimeout(timeout: number): Promise<void> {\n    this.log(`Waiting for ${timeout}ms`)\n    await wait(timeout)\n  }\n\n  private async waitSelector(\n    selector: string,\n    waitTimeout: number = this.chromelessOptions.waitTimeout,\n  ): Promise<void> {\n    this.log(`Waiting for ${selector} ${waitTimeout}`)\n    await waitForNode(this.client, selector, waitTimeout)\n    this.log(`Waited for ${selector}`)\n  }\n\n  private async click(selector: string, x?: number, y?: number): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`click(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(`click(): node for selector ${selector} doesn't exist`)\n    }\n\n    const { scale } = this.chromelessOptions.viewport\n    if (this.chromelessOptions.scrollBeforeClick) {\n      await scrollToElement(this.client, selector)\n    }\n    await click(this.client, selector, scale, x, y)\n    this.log(`Clicked on ${selector} at (${x}, ${y})`)\n  }\n\n  private async returnCode<T>(fn: string, ...args: any[]): Promise<T> {\n    return (await evaluate(this.client, fn, ...args)) as T\n  }\n\n  private async scrollTo<T>(x: number, y: number): Promise<void> {\n    return scrollTo(this.client, x, y)\n  }\n\n  private async scrollToElement<T>(selector: string): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`scrollToElement(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n    return scrollToElement(this.client, selector)\n  }\n\n  private async mousedown(selector: string): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`mousedown(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(\n        `mousedown(): node for selector ${selector} doesn't exist`,\n      )\n    }\n\n    const { scale } = this.chromelessOptions.viewport\n    await mousedown(this.client, selector, scale)\n    this.log(`Mousedown on ${selector}`)\n  }\n\n  private async mouseup(selector: string): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`mouseup(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(`mouseup(): node for selector ${selector} doesn't exist`)\n    }\n\n    const { scale } = this.chromelessOptions.viewport\n    await mouseup(this.client, selector, scale)\n    this.log(`Mouseup on ${selector}`)\n  }\n\n  private async setHtml(html: string): Promise<void> {\n    await setHtml(this.client, html)\n  }\n\n  private async focus(selector: string): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`focus(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(`focus(): node for selector ${selector} doesn't exist`)\n    }\n\n    await focus(this.client, selector)\n    this.log(`Focus on ${selector}`)\n  }\n\n  async type(text: string, selector?: string): Promise<void> {\n    if (selector) {\n      if (this.chromelessOptions.implicitWait) {\n        this.log(`type(): Waiting for ${selector}`)\n        await waitForNode(\n          this.client,\n          selector,\n          this.chromelessOptions.waitTimeout,\n        )\n      }\n\n      const exists = await nodeExists(this.client, selector)\n      if (!exists) {\n        throw new Error(`type(): Node not found for selector: ${selector}`)\n      }\n    }\n    await type(this.client, text, selector)\n    this.log(`Typed ${text} in ${selector}`)\n  }\n\n  async cookies(nameOrQuery?: string | CookieQuery): Promise<Cookie[]> {\n    return await getCookies(this.client, nameOrQuery as string | undefined)\n  }\n\n  async allCookies(): Promise<Cookie[]> {\n    return await getAllCookies(this.client)\n  }\n\n  async setExtraHTTPHeaders(headers: Headers): Promise<void> {\n    return await setExtraHTTPHeaders(this.client, headers)\n  }\n\n  async setCookies(\n    nameOrCookies: string | Cookie | Cookie[],\n    value?: string,\n  ): Promise<void> {\n    if (typeof nameOrCookies !== 'string' && !value) {\n      const cookies = Array.isArray(nameOrCookies)\n        ? nameOrCookies\n        : [nameOrCookies]\n      return await setCookies(this.client, cookies)\n    }\n\n    if (typeof nameOrCookies === 'string' && typeof value === 'string') {\n      const fn = () => location.href\n      const url = (await evaluate(this.client, `${fn}`)) as string\n      const cookie: Cookie = {\n        url,\n        name: nameOrCookies,\n        value,\n      }\n      return await setCookies(this.client, [cookie])\n    }\n\n    throw new Error(`setCookies(): Invalid input ${nameOrCookies}, ${value}`)\n  }\n\n  async deleteCookies(name: string, url: string): Promise<void> {\n    const { Network } = this.client\n    const canClearCookies = await Network.canClearBrowserCookies()\n    if (canClearCookies) {\n      await deleteCookie(this.client, name, url)\n      this.log(`Cookie ${name} cleared`)\n    } else {\n      this.log(`Cookie ${name} could not be cleared`)\n    }\n  }\n\n  async clearCookies(): Promise<void> {\n    const { Network } = this.client\n    const canClearCookies = await Network.canClearBrowserCookies()\n    if (canClearCookies) {\n      await clearCookies(this.client)\n      this.log('Cookies cleared')\n    } else {\n      this.log('Cookies could not be cleared')\n    }\n  }\n\n  async press(keyCode: number, count?: number, modifiers?: any): Promise<void> {\n    this.log(`Sending keyCode ${keyCode} (modifiers: ${modifiers})`)\n    await press(this.client, keyCode, count, modifiers)\n  }\n\n  async returnExists(selector: string): Promise<boolean> {\n    return await nodeExists(this.client, selector)\n  }\n\n  async returnInputValue(selector: string): Promise<string> {\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(`value: node for selector ${selector} doesn't exist`)\n    }\n    return getValue(this.client, selector)\n  }\n\n  // Returns the S3 url or local file path\n  async returnScreenshot(\n    selector?: string,\n    options?: ScreenshotOptions,\n  ): Promise<string> {\n    if (selector) {\n      if (this.chromelessOptions.implicitWait) {\n        this.log(`screenshot(): Waiting for ${selector}`)\n        await waitForNode(\n          this.client,\n          selector,\n          this.chromelessOptions.waitTimeout,\n        )\n      }\n\n      const exists = await nodeExists(this.client, selector)\n      if (!exists) {\n        throw new Error(\n          `screenshot(): node for selector ${selector} doesn't exist`,\n        )\n      }\n    }\n\n    const data = await screenshot(this.client, selector, options)\n\n    if (isS3Configured()) {\n      return await uploadToS3(data, 'image/png')\n    } else {\n      return writeToFile(data, 'png', options && options.filePath)\n    }\n  }\n\n  async returnHtml(): Promise<string> {\n    return await html(this.client)\n  }\n\n  async returnHtmlUrl(options?: { filePath?: string }): Promise<string> {\n    const data = await html(this.client)\n\n    if (isS3Configured()) {\n      return await uploadToS3(data, 'text/html')\n    } else {\n      return writeToFile(data, 'html', options && options.filePath)\n    }\n  }\n\n  // Returns the S3 url or local file path\n  async returnPdf(options?: PdfOptions): Promise<string> {\n    const { filePath, ...cdpOptions } = options || { filePath: undefined }\n    const data = await pdf(this.client, cdpOptions)\n\n    if (isS3Configured()) {\n      return await uploadToS3(data, 'application/pdf')\n    } else {\n      return writeToFile(data, 'pdf', filePath)\n    }\n  }\n\n  async clearInput(selector: string): Promise<void> {\n    if (selector) {\n      if (this.chromelessOptions.implicitWait) {\n        this.log(`clearInput(): Waiting for ${selector}`)\n        await waitForNode(\n          this.client,\n          selector,\n          this.chromelessOptions.waitTimeout,\n        )\n      }\n\n      const exists = await nodeExists(this.client, selector)\n      if (!exists) {\n        throw new Error(\n          `clearInput(): Node not found for selector: ${selector}`,\n        )\n      }\n    }\n    await clearInput(this.client, selector)\n    this.log(`${selector} cleared`)\n  }\n\n  async setFileInput(selector: string, files: string[]): Promise<void> {\n    if (this.chromelessOptions.implicitWait) {\n      this.log(`setFileInput(): Waiting for ${selector}`)\n      await waitForNode(\n        this.client,\n        selector,\n        this.chromelessOptions.waitTimeout,\n      )\n    }\n\n    const exists = await nodeExists(this.client, selector)\n    if (!exists) {\n      throw new Error(\n        `setFileInput(): node for selector ${selector} doesn't exist`,\n      )\n    }\n\n    await setFileInput(this.client, selector, files)\n    this.log(`setFileInput() files ${files}`)\n  }\n\n  private log(msg: string): void {\n    if (this.chromelessOptions.debug) {\n      console.log(msg)\n    }\n  }\n}\n"
  },
  {
    "path": "src/chrome/local.ts",
    "content": "import { Chrome, Command, ChromelessOptions, Client } from '../types'\nimport * as CDP from 'chrome-remote-interface'\nimport { LaunchedChrome, launch } from 'chrome-launcher'\nimport LocalRuntime from './local-runtime'\nimport { evaluate, setViewport } from '../util'\nimport { DeviceMetrics } from '../types'\n\ninterface RuntimeClient {\n  client: Client\n  runtime: LocalRuntime\n}\n\nexport default class LocalChrome implements Chrome {\n  private options: ChromelessOptions\n  private runtimeClientPromise: Promise<RuntimeClient>\n  private chromeInstance?: LaunchedChrome\n\n  constructor(options: ChromelessOptions = {}) {\n    this.options = options\n\n    this.runtimeClientPromise = this.initRuntimeClient()\n  }\n\n  private async initRuntimeClient(): Promise<RuntimeClient> {\n    const client = this.options.launchChrome\n      ? await this.startChrome()\n      : await this.connectToChrome()\n\n    const { viewport = {} as DeviceMetrics } = this.options\n    await setViewport(client, viewport as DeviceMetrics)\n\n    const runtime = new LocalRuntime(client, this.options)\n\n    return { client, runtime }\n  }\n\n  private async startChrome(): Promise<Client> {\n    const { port } = this.options.cdp\n    this.chromeInstance = await launch({\n      logLevel: this.options.debug ? 'info' : 'silent',\n      chromeFlags: [\n        // Do not render scroll bars\n        '--hide-scrollbars',\n\n        // The following options copied verbatim from https://github.com/GoogleChrome/chrome-launcher/blob/master/src/flags.ts\n\n        // Disable built-in Google Translate service\n        '--disable-translate',\n        // Disable all chrome extensions entirely\n        '--disable-extensions',\n        // Disable various background network services, including extension updating,\n        //   safe browsing service, upgrade detector, translate, UMA\n        '--disable-background-networking',\n        // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking\n        '--safebrowsing-disable-auto-update',\n        // Disable syncing to a Google account\n        '--disable-sync',\n        // Disable reporting to UMA, but allows for collection\n        '--metrics-recording-only',\n        // Mute any audio\n        '--mute-audio',\n        // Skip first run wizards\n        '--no-first-run',\n      ],\n      port,\n    })\n    const target = await CDP.New({\n      port,\n    })\n    return await CDP({ target, port })\n  }\n\n  private async connectToChrome(): Promise<Client> {\n    const { host, port } = this.options.cdp\n    const target = await CDP.New({\n      port,\n      host,\n    })\n    return await CDP({ target, host, port })\n  }\n\n  private async setViewport(client: Client) {\n    const { viewport = {} } = this.options\n\n    const config: any = {\n      deviceScaleFactor: 1,\n      mobile: false,\n      scale: viewport.scale || 1,\n      fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable\n    }\n\n    const { host, port } = this.options.cdp\n    const versionResult = await CDP.Version({ host, port })\n    const isHeadless = versionResult['User-Agent'].includes('Headless')\n\n    if (viewport.height && viewport.width) {\n      config.height = viewport.height\n      config.width = viewport.width\n    } else if (isHeadless) {\n      // just apply default value in headless mode to maintain original browser viewport\n      config.height = 900\n      config.width = 1440\n    } else {\n      config.height = await evaluate(\n        client,\n        (() => window.innerHeight).toString(),\n      )\n      config.width = await evaluate(\n        client,\n        (() => window.innerWidth).toString(),\n      )\n    }\n\n    await client.Emulation.setDeviceMetricsOverride(config)\n    await client.Emulation.setVisibleSize({\n      width: config.width,\n      height: config.height,\n    })\n  }\n\n  async process<T extends any>(command: Command): Promise<T> {\n    const { runtime } = await this.runtimeClientPromise\n\n    return (await runtime.run(command)) as T\n  }\n\n  async close(): Promise<void> {\n    const { client } = await this.runtimeClientPromise\n\n    if (this.options.cdp.closeTab) {\n      const { host, port } = this.options.cdp\n      await CDP.Close({ host, port, id: client.target.id })\n    }\n\n    if (this.chromeInstance) {\n      this.chromeInstance.kill()\n    }\n\n    await client.close()\n  }\n}\n"
  },
  {
    "path": "src/chrome/remote.ts",
    "content": "import { Chrome, ChromelessOptions, Command, RemoteOptions } from '../types'\nimport { connect as mqtt, MqttClient } from 'mqtt'\nimport * as cuid from 'cuid'\nimport * as got from 'got'\n\ninterface RemoteResult {\n  value?: any\n  error?: string\n}\n\nfunction getEndpoint(remoteOptions: RemoteOptions | boolean): RemoteOptions {\n  if (typeof remoteOptions === 'object' && remoteOptions.endpointUrl) {\n    return remoteOptions\n  }\n\n  if (\n    process.env['CHROMELESS_ENDPOINT_URL'] &&\n    process.env['CHROMELESS_ENDPOINT_API_KEY']\n  ) {\n    return {\n      endpointUrl: process.env['CHROMELESS_ENDPOINT_URL'],\n      apiKey: process.env['CHROMELESS_ENDPOINT_API_KEY'],\n    }\n  }\n\n  throw new Error(\n    '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.',\n  )\n}\n\nexport default class RemoteChrome implements Chrome {\n  private options: ChromelessOptions\n  private channelId: string\n  private channel: MqttClient\n  private connectionPromise: Promise<void>\n  private TOPIC_NEW_SESSION: string\n  private TOPIC_CONNECTED: string\n  private TOPIC_REQUEST: string\n  private TOPIC_RESPONSE: string\n  private TOPIC_END: string\n\n  constructor(options: ChromelessOptions) {\n    this.options = options\n    this.connectionPromise = this.initConnection()\n  }\n\n  private async initConnection(): Promise<void> {\n    await new Promise(async (resolve, reject) => {\n      const timeout = setTimeout(() => {\n        if (this.channel) {\n          this.channel.end()\n        }\n\n        reject(\n          new Error(\n            \"Timed out after 30sec. Connection couldn't be established.\",\n          ),\n        )\n      }, 30000)\n\n      try {\n        const { endpointUrl, apiKey } = getEndpoint(this.options.remote)\n        const { body: { url, channelId } } = await got(endpointUrl, {\n          headers: apiKey\n            ? {\n                'x-api-key': apiKey,\n              }\n            : undefined,\n          json: true,\n        })\n\n        this.channelId = channelId\n\n        this.TOPIC_NEW_SESSION = 'chrome/new-session'\n        this.TOPIC_CONNECTED = `chrome/${channelId}/connected`\n        this.TOPIC_REQUEST = `chrome/${channelId}/request`\n        this.TOPIC_RESPONSE = `chrome/${channelId}/response`\n        this.TOPIC_END = `chrome/${channelId}/end`\n\n        const channel = mqtt(url, {\n          will: {\n            topic: 'chrome/last-will',\n            payload: JSON.stringify({ channelId }),\n            qos: 1,\n            retain: false,\n          },\n        })\n\n        this.channel = channel\n\n        if (this.options.debug) {\n          channel.on('error', error => console.log('WebSocket error', error))\n          channel.on('offline', () => console.log('WebSocket offline'))\n        }\n\n        channel.on('connect', () => {\n          if (this.options.debug) {\n            console.log('Connected to message broker.')\n          }\n\n          channel.subscribe(this.TOPIC_CONNECTED, { qos: 1 }, () => {\n            channel.on('message', async topic => {\n              if (this.TOPIC_CONNECTED === topic) {\n                clearTimeout(timeout)\n                resolve()\n              }\n            })\n\n            channel.publish(\n              this.TOPIC_NEW_SESSION,\n              JSON.stringify({ channelId, options: this.options }),\n              { qos: 1 },\n            )\n          })\n\n          channel.subscribe(this.TOPIC_END, () => {\n            channel.on('message', async (topic, buffer) => {\n              if (this.TOPIC_END === topic) {\n                const message = buffer.toString()\n                const data = JSON.parse(message)\n\n                if (data.outOfTime) {\n                  console.warn(\n                    `Chromeless Proxy disconnected because it reached it's execution time limit (5 minutes).`,\n                  )\n                } else if (data.inactivity) {\n                  console.warn(\n                    'Chromeless Proxy disconnected due to inactivity (no commands sent for 30 seconds).',\n                  )\n                } else {\n                  console.warn(\n                    `Chromeless Proxy disconnected (we don't know why).`,\n                    data,\n                  )\n                }\n\n                await this.close()\n              }\n            })\n          })\n        })\n      } catch (error) {\n        console.error(error)\n\n        reject(\n          new Error('Unable to get presigned websocket URL and connect to it.'),\n        )\n      }\n    })\n  }\n\n  async process<T extends any>(command: Command): Promise<T> {\n    // wait until lambda connection is established\n    await this.connectionPromise\n\n    if (this.options.debug) {\n      console.log(`Running remotely: ${JSON.stringify(command)}`)\n    }\n\n    const promise = new Promise<T>((resolve, reject) => {\n      this.channel.subscribe(this.TOPIC_RESPONSE, () => {\n        this.channel.on('message', (topic, buffer) => {\n          if (this.TOPIC_RESPONSE === topic) {\n            const message = buffer.toString()\n            const result = JSON.parse(message) as RemoteResult\n\n            if (result.error) {\n              reject(result.error)\n            } else if (result.value) {\n              resolve(result.value)\n            } else {\n              resolve()\n            }\n          }\n        })\n        this.channel.publish(this.TOPIC_REQUEST, JSON.stringify(command))\n      })\n    })\n\n    return promise\n  }\n\n  async close(): Promise<void> {\n    this.channel.publish(\n      this.TOPIC_END,\n      JSON.stringify({ channelId: this.channelId, client: true }),\n    )\n\n    this.channel.end()\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import Chromeless from './api'\nimport Queue from './queue'\nimport LocalChrome from './chrome/local'\nimport { version } from './util'\nimport { Cookie, ChromelessOptions } from './types'\n\nexport { Queue, LocalChrome, Chromeless, Cookie, ChromelessOptions, version }\n\nexport default Chromeless\n"
  },
  {
    "path": "src/queue.ts",
    "content": "import { Chrome, Command } from './types'\n\nexport default class Queue {\n  private flushCount: number\n  private commandQueue: {\n    [flushCount: number]: Command[]\n  }\n  private chrome: Chrome\n  private lastWaitAll: Promise<void>\n\n  constructor(chrome: Chrome) {\n    this.chrome = chrome\n    this.flushCount = 0\n    this.commandQueue = {\n      0: [],\n    }\n  }\n\n  async end(): Promise<void> {\n    this.lastWaitAll = this.waitAll()\n    await this.lastWaitAll\n\n    await this.chrome.close()\n  }\n\n  enqueue(command: Command): void {\n    this.commandQueue[this.flushCount].push(command)\n  }\n\n  async process<T extends any>(command: Command): Promise<T> {\n    // with lastWaitAll we build a promise chain\n    // already change the pointer to lastWaitAll for the next .process() call\n    // after the pointer is set, wait for the previous tasks\n    // then wait for the own pointer (the new .lastWaitAll)\n    if (this.lastWaitAll) {\n      const lastWaitAllTmp = this.lastWaitAll\n      this.lastWaitAll = this.waitAll()\n      await lastWaitAllTmp\n    } else {\n      this.lastWaitAll = this.waitAll()\n    }\n\n    await this.lastWaitAll\n\n    return this.chrome.process<T>(command)\n  }\n\n  private async waitAll(): Promise<void> {\n    const previousFlushCount = this.flushCount\n\n    this.flushCount++\n    this.commandQueue[this.flushCount] = []\n\n    for (const command of this.commandQueue[previousFlushCount]) {\n      await this.chrome.process(command)\n    }\n  }\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export interface Client {\n  Network: any\n  Page: any\n  DOM: any\n  Input: any\n  Runtime: any\n  Emulation: any\n  Storage: any\n  close: () => void\n  target: {\n    id: string\n  }\n  port: any\n  host: any\n}\n\nexport interface DeviceMetrics {\n  width: number\n  height: number\n  deviceScaleFactor?: number\n  mobile?: boolean\n  scale?: number\n  screenOrientation?: ScreenOrientation\n}\n\nexport interface ScreenOrientation {\n  type: string\n  angle: number\n}\n\nexport interface RemoteOptions {\n  endpointUrl: string\n  apiKey?: string\n}\n\nexport interface CDPOptions {\n  host?: string // localhost\n  port?: number // 9222\n  secure?: boolean // false\n  closeTab?: boolean // true\n}\n\nexport interface ChromelessOptions {\n  debug?: boolean // false\n  waitTimeout?: number // 10000ms\n  implicitWait?: boolean // false\n  scrollBeforeClick?: boolean // false\n  viewport?: {\n    width?: number // 1440 if headless\n    height?: number // 900 if headless\n    scale?: number // 1\n  }\n  launchChrome?: boolean // auto-launch chrome (local) `true`\n  cdp?: CDPOptions\n  remote?: RemoteOptions | boolean\n}\n\nexport interface Chrome {\n  process<T extends any>(command: Command): Promise<T>\n  close(): Promise<void>\n}\n\nexport type Command =\n  | {\n      type: 'goto'\n      url: string\n      timeout?: number\n    }\n  | {\n      type: 'clearCache'\n    }\n  | {\n      type: 'setViewport'\n      options: DeviceMetrics\n    }\n  | {\n      type: 'setUserAgent'\n      useragent: string\n    }\n  | {\n      type: 'wait'\n      timeout?: number\n      selector?: string\n      fn?: string\n      args?: any[]\n    }\n  | {\n      type: 'click'\n      selector: string\n      x?: number\n      y?: number\n    }\n  | {\n      type: 'returnCode'\n      fn: string\n      args?: any[]\n    }\n  | {\n      type: 'returnInputValue'\n      selector: string\n    }\n  | {\n      type: 'returnExists'\n      selector: string\n    }\n  | {\n      type: 'returnValue'\n      selector: string\n    }\n  | {\n      type: 'returnScreenshot'\n      selector?: string\n      options?: ScreenshotOptions\n    }\n  | {\n      type: 'returnHtml'\n    }\n  | {\n      type: 'returnHtmlUrl'\n    }\n  | {\n      type: 'returnPdf'\n      options?: PdfOptions\n    }\n  | {\n      type: 'scrollTo'\n      x: number\n      y: number\n    }\n  | {\n      type: 'scrollToElement'\n      selector: string\n    }\n  | {\n      type: 'setHtml'\n      html: string\n    }\n  | {\n      type: 'setExtraHTTPHeaders'\n      headers: Headers\n    }\n  | {\n      type: 'press'\n      keyCode: number\n      count?: number\n      modifiers?: any\n    }\n  | {\n      type: 'type'\n      input: string\n      selector?: string\n    }\n  | {\n      type: 'clearCookies'\n    }\n  | {\n      type: 'clearStorage'\n      origin: string\n      storageTypes: string\n    }\n  | {\n      type: 'deleteCookies'\n      name: string\n      url: string\n    }\n  | {\n      type: 'setCookies'\n      nameOrCookies: string | Cookie | Cookie[]\n      value?: string\n    }\n  | {\n      type: 'allCookies'\n    }\n  | {\n      type: 'cookies'\n      nameOrQuery?: string | CookieQuery\n    }\n  | {\n      type: 'mousedown'\n      selector: string\n    }\n  | {\n      type: 'mouseup'\n      selector: string\n    }\n  | {\n      type: 'focus'\n      selector: string\n    }\n  | {\n      type: 'clearInput'\n      selector: string\n    }\n  | {\n      type: 'setFileInput'\n      selector: string\n      files: string[]\n    }\n\nexport type Headers = Record<string, string>\n\nexport interface Cookie {\n  url?: string\n  domain?: string\n  name: string\n  value: string\n  path?: string\n  expires?: number\n  size?: number\n  httpOnly?: boolean\n  secure?: boolean\n  session?: boolean\n}\n\nexport interface CookieQuery {\n  name: string\n  path?: string\n  expires?: number\n  size?: number\n  httpOnly?: boolean\n  secure?: boolean\n  session?: boolean\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF\nexport interface PdfOptions {\n  landscape?: boolean\n  displayHeaderFooter?: boolean\n  printBackground?: boolean\n  scale?: number\n  paperWidth?: number\n  paperHeight?: number\n  marginTop?: number\n  marginBottom?: number\n  marginLeft?: number\n  marginRight?: number\n  pageRanges?: string\n  ignoreInvalidPageRanges?: boolean\n  filePath?: string // for internal use\n}\n\nexport interface ScreenshotOptions {\n  filePath?: string\n  omitBackground?: boolean\n}\n\nexport type Quad = Array<number>\n\nexport interface ShapeOutsideInfo {\n  bounds: Quad\n  shape: Array<any>\n  marginShape: Array<any>\n}\n\nexport interface BoxModel {\n  content: Quad\n  padding: Quad\n  border: Quad\n  margin: Quad\n  width: number\n  height: number\n  shapeOutside: ShapeOutsideInfo\n}\n\nexport interface Viewport {\n  x: number\n  y: number\n  width: number\n  height: number\n  scale: number\n}\n"
  },
  {
    "path": "src/util.test.ts",
    "content": "import * as fs from 'fs'\nimport * as os from 'os'\nimport * as CDP from 'chrome-remote-interface'\nimport test from 'ava'\nimport Chromeless from '../src'\n\nconst testHtml = fs.readFileSync('./src/__tests__/test.html')\nconst testUrl = `data:text/html,${testHtml}`\n\nconst getPngMetaData = async (filePath): Promise<any> => {\n  const fd = fs.openSync(filePath, 'r')\n  return await new Promise(resolve => {\n    fs.read(fd, Buffer.alloc(24), 0, 24, 0, (err, bytesRead, buffer) =>\n      resolve({\n        width: buffer.readUInt32BE(16),\n        height: buffer.readUInt32BE(20),\n      }),\n    )\n  })\n}\n\n// POC\ntest('evaluate (document.title)', async t => {\n  const chromeless = new Chromeless({ launchChrome: false })\n  const title = await chromeless.goto(testUrl).evaluate(() => document.title)\n\n  await chromeless.end()\n\n  t.is(title, 'Title')\n})\n\ntest('screenshot and pdf path', async t => {\n  const chromeless = new Chromeless({ launchChrome: false })\n  const screenshot = await chromeless.goto(testUrl).screenshot()\n  const pdf = await chromeless.goto(testUrl).pdf()\n\n  await chromeless.end()\n\n  const regex = new RegExp(os.tmpdir().replace(/\\\\/g, '\\\\\\\\'))\n\n  t.regex(screenshot, regex)\n  t.regex(pdf, regex)\n})\n\ntest('screenshot by selector', async t => {\n  const version = await CDP.Version()\n  const versionMajor = parseInt(/Chrome\\/(\\d+)/.exec(version['User-Agent'])[1])\n  // clipping will only work on chrome 61+\n\n  const chromeless = new Chromeless({ launchChrome: false })\n  const screenshot = await chromeless.goto(testUrl).screenshot('img')\n\n  await chromeless.end()\n\n  const png = await getPngMetaData(screenshot)\n  t.is(png.width, versionMajor > 60 ? 512 : 1440)\n  t.is(png.height, versionMajor > 60 ? 512 : 900)\n})\n"
  },
  {
    "path": "src/util.ts",
    "content": "import * as fs from 'fs'\nimport * as os from 'os'\nimport * as path from 'path'\nimport * as cuid from 'cuid'\nimport {\n  Client,\n  Cookie,\n  DeviceMetrics,\n  PdfOptions,\n  BoxModel,\n  Viewport,\n  Headers,\n  ScreenshotOptions,\n} from './types'\nimport * as CDP from 'chrome-remote-interface'\nimport * as AWS from 'aws-sdk'\n\nexport const version: string = ((): string => {\n  if (fs.existsSync(path.join(__dirname, '../package.json'))) {\n    // development (look in /src)\n    return require('../package.json').version\n  } else {\n    // production (look in /dist/src)\n    return require('../../package.json').version\n  }\n})()\n\nexport async function setViewport(\n  client: Client,\n  viewport: DeviceMetrics = { width: 1, height: 1, scale: 1 },\n): Promise<void> {\n  const config: any = {\n    deviceScaleFactor: 1,\n    mobile: false,\n    scale: viewport.scale || 1,\n    fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable\n  }\n\n  const { host, port } = client\n  const versionResult = await CDP.Version({ host, port })\n  const isHeadless = versionResult['User-Agent'].includes('Headless')\n\n  if (viewport.height && viewport.width) {\n    config.height = viewport.height\n    config.width = viewport.width\n  } else if (isHeadless) {\n    // just apply default value in headless mode to maintain original browser viewport\n    config.height = 900\n    config.width = 1440\n  } else {\n    config.height = await evaluate(\n      client,\n      (() => window.innerHeight).toString(),\n    )\n    config.width = await evaluate(client, (() => window.innerWidth).toString())\n  }\n\n  await client.Emulation.setDeviceMetricsOverride(config)\n  await client.Emulation.setVisibleSize({\n    width: config.width,\n    height: config.height,\n  })\n  return\n}\n\nexport async function waitForNode(\n  client: Client,\n  selector: string,\n  waitTimeout: number,\n): Promise<void> {\n  const { Runtime } = client\n  const getNode = `selector => {\n    return document.querySelector(selector)\n  }`\n\n  const result = await Runtime.evaluate({\n    expression: `(${getNode})(\\`${selector}\\`)`,\n  })\n\n  if (result.result.value === null) {\n    const start = new Date().getTime()\n    return new Promise<void>((resolve, reject) => {\n      const interval = setInterval(async () => {\n        if (new Date().getTime() - start > waitTimeout) {\n          clearInterval(interval)\n          reject(\n            new Error(`wait(\"${selector}\") timed out after ${waitTimeout}ms`),\n          )\n        }\n\n        const result = await Runtime.evaluate({\n          expression: `(${getNode})(\\`${selector}\\`)`,\n        })\n\n        if (result.result.value !== null) {\n          clearInterval(interval)\n          resolve()\n        }\n      }, 500)\n    })\n  } else {\n    return\n  }\n}\n\nexport async function wait(timeout: number): Promise<void> {\n  return new Promise<void>((resolve, reject) => setTimeout(resolve, timeout))\n}\n\nexport async function waitForPromise<T>(\n  promise: Promise<T>,\n  waitTimeout: number,\n  label?: string,\n): Promise<T> {\n  return new Promise<T>((resolve, reject) => {\n    let fullfilled = false\n    setTimeout(() => {\n      fullfilled = true\n      reject(\n        new Error(\n          `wait(${label || 'Promise'}) timed out after ${waitTimeout}ms`,\n        ),\n      )\n    }, waitTimeout)\n    return promise\n      .then(res => (fullfilled ? void 0 : resolve(res)))\n      .catch(err => (fullfilled ? void 0 : reject(err)))\n  })\n}\n\nexport function eventToPromise() {\n  let resolve\n  const promise = new Promise(res => {\n    resolve = res\n  })\n  return {\n    onEvent(...args) {\n      resolve(args.length > 1 ? args : args[0])\n    },\n    fired() {\n      return promise\n    },\n  }\n}\n\nexport async function nodeExists(\n  client: Client,\n  selector: string,\n): Promise<boolean> {\n  const { Runtime } = client\n  const exists = `selector => {\n    return !!document.querySelector(selector)\n  }`\n\n  const expression = `(${exists})(\\`${selector}\\`)`\n\n  const result = await Runtime.evaluate({\n    expression,\n  })\n\n  return result.result.value\n}\n\nexport async function getClientRect(client, selector): Promise<ClientRect> {\n  const { Runtime } = client\n\n  const code = `selector => {\n    const element = document.querySelector(selector)\n    if (!element) {\n      return undefined\n    }\n\n    const rect = element.getBoundingClientRect()\n    return JSON.stringify({\n      left: rect.left,\n      top: rect.top,\n      right: rect.right,\n      bottom: rect.bottom,\n      height: rect.height,\n      width: rect.width,\n    })\n  }`\n\n  const expression = `(${code})(\\`${selector}\\`)`\n  const result = await Runtime.evaluate({ expression })\n\n  if (!result.result.value) {\n    throw new Error(`No element found for selector: ${selector}`)\n  }\n\n  return JSON.parse(result.result.value) as ClientRect\n}\n\nexport async function click(\n  client: Client,\n  selector: string,\n  scale: number,\n  x?: number,\n  y?: number,\n) {\n  const clientRect = await getClientRect(client, selector)\n  const { Input } = client\n  if (x === undefined) x = clientRect.width / 2\n  if (y === undefined) y = clientRect.height / 2\n  const options = {\n    x: Math.round((clientRect.left + x) * scale),\n    y: Math.round((clientRect.top + y) * scale),\n    button: 'left',\n    clickCount: 1,\n  }\n\n  await Input.dispatchMouseEvent({\n    ...options,\n    type: 'mousePressed',\n  })\n  await Input.dispatchMouseEvent({\n    ...options,\n    type: 'mouseReleased',\n  })\n}\n\nexport async function focus(client: Client, selector: string): Promise<void> {\n  const { DOM } = client\n  const dom = await DOM.getDocument()\n  const node = await DOM.querySelector({\n    nodeId: dom.root.nodeId,\n    selector: selector,\n  })\n  await DOM.focus(node)\n}\n\nexport async function evaluate<T>(\n  client: Client,\n  fn: string,\n  ...args: any[]\n): Promise<T> {\n  const { Runtime } = client\n  const jsonArgs = JSON.stringify(args)\n  const argStr = jsonArgs.substr(1, jsonArgs.length - 2)\n\n  const expression = `\n    (() => {\n      const expressionResult = (${fn})(${argStr});\n      if (expressionResult && expressionResult.then) {\n        expressionResult.catch((error) => { throw new Error(error); });\n        return expressionResult;\n      }\n      return Promise.resolve(expressionResult);\n    })();\n  `\n\n  const result = await Runtime.evaluate({\n    expression,\n    returnByValue: true,\n    awaitPromise: true,\n  })\n\n  if (result && result.exceptionDetails) {\n    throw new Error(\n      result.exceptionDetails.exception.value ||\n        result.exceptionDetails.exception.description,\n    )\n  }\n\n  if (result && result.result) {\n    return result.result.value\n  }\n\n  return null\n}\n\nexport async function type(\n  client: Client,\n  text: string,\n  selector?: string,\n): Promise<void> {\n  if (selector) {\n    await focus(client, selector)\n    await wait(500)\n  }\n\n  const { Input } = client\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i]\n    const options = {\n      type: 'char',\n      text: char,\n      unmodifiedText: char,\n    }\n    await Input.dispatchKeyEvent(options)\n  }\n}\n\nexport async function press(\n  client: Client,\n  keyCode: number,\n  count?: number,\n  modifiers?: any,\n): Promise<void> {\n  const { Input } = client\n\n  if (count === undefined) {\n    count = 1\n  }\n\n  const options = {\n    nativeVirtualKeyCode: keyCode,\n    windowsVirtualKeyCode: keyCode,\n  }\n\n  if (modifiers) {\n    options['modifiers'] = modifiers\n  }\n\n  for (let i = 0; i < count; i++) {\n    await Input.dispatchKeyEvent({\n      ...options,\n      type: 'rawKeyDown',\n    })\n    await Input.dispatchKeyEvent({\n      ...options,\n      type: 'keyUp',\n    })\n  }\n}\n\nexport async function getValue(\n  client: Client,\n  selector: string,\n): Promise<string> {\n  const { Runtime } = client\n  const browserCode = `selector => {\n    return document.querySelector(selector).value\n  }`\n  const expression = `(${browserCode})(\\`${selector}\\`)`\n  const result = await Runtime.evaluate({\n    expression,\n  })\n\n  return result.result.value\n}\n\nexport async function scrollTo(\n  client: Client,\n  x: number,\n  y: number,\n): Promise<void> {\n  const { Runtime } = client\n  const browserCode = `(x, y) => {\n    return window.scrollTo(x, y)\n  }`\n  const expression = `(${browserCode})(${x}, ${y})`\n  await Runtime.evaluate({\n    expression,\n  })\n}\n\nexport async function scrollToElement(\n  client: Client,\n  selector: string,\n): Promise<void> {\n  const clientRect = await getClientRect(client, selector)\n\n  return scrollTo(client, clientRect.left, clientRect.top)\n}\n\nexport async function setHtml(client: Client, html: string): Promise<void> {\n  const { Page } = client\n\n  const { frameTree: { frame: { id: frameId } } } = await Page.getResourceTree()\n  await Page.setDocumentContent({ frameId, html })\n}\n\nexport async function getCookies(\n  client: Client,\n  nameOrQuery?: string | Cookie,\n): Promise<any> {\n  const { Network } = client\n\n  const fn = () => location.href\n\n  const url = (await evaluate(client, `${fn}`)) as string\n\n  const result = await Network.getCookies([url])\n  const cookies = result.cookies\n\n  if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery === 'string') {\n    const filteredCookies: Cookie[] = cookies.filter(\n      cookie => cookie.name === nameOrQuery,\n    )\n    return filteredCookies\n  }\n  return cookies\n}\n\nexport async function getAllCookies(client: Client): Promise<any> {\n  const { Network } = client\n\n  const result = await Network.getAllCookies()\n  return result.cookies\n}\n\nexport async function setCookies(\n  client: Client,\n  cookies: Cookie[],\n): Promise<void> {\n  const { Network } = client\n\n  for (const cookie of cookies) {\n    await Network.setCookie({\n      ...cookie,\n      url: cookie.url ? cookie.url : getUrlFromCookie(cookie),\n    })\n  }\n}\n\nexport async function setExtraHTTPHeaders(\n  client: Client,\n  headers: Headers,\n): Promise<void> {\n  const { Network } = client\n  await Network.setExtraHTTPHeaders({ headers })\n}\n\nexport async function mousedown(\n  client: Client,\n  selector: string,\n  scale: number,\n) {\n  const clientRect = await getClientRect(client, selector)\n  const { Input } = client\n\n  const options = {\n    x: Math.round((clientRect.left + clientRect.width / 2) * scale),\n    y: Math.round((clientRect.top + clientRect.height / 2) * scale),\n    button: 'left',\n    clickCount: 1,\n  }\n\n  await Input.dispatchMouseEvent({\n    ...options,\n    type: 'mousePressed',\n  })\n}\n\nexport async function mouseup(client: Client, selector: string, scale: number) {\n  const clientRect = await getClientRect(client, selector)\n  const { Input } = client\n\n  const options = {\n    x: Math.round((clientRect.left + clientRect.width / 2) * scale),\n    y: Math.round((clientRect.top + clientRect.height / 2) * scale),\n    button: 'left',\n    clickCount: 1,\n  }\n\n  await Input.dispatchMouseEvent({\n    ...options,\n    type: 'mouseReleased',\n  })\n}\n\nfunction getUrlFromCookie(cookie: Cookie) {\n  const domain = cookie.domain.slice(1, cookie.domain.length)\n  return `https://${domain}`\n}\n\nexport async function deleteCookie(\n  client: Client,\n  name: string,\n  url: string,\n): Promise<void> {\n  const { Network } = client\n\n  await Network.deleteCookie({ cookieName: name, url })\n}\n\nexport async function clearCookies(client: Client): Promise<void> {\n  const { Network } = client\n\n  await Network.clearBrowserCookies()\n}\n\nexport async function getBoxModel(\n  client: Client,\n  selector: string,\n): Promise<BoxModel> {\n  const { DOM } = client\n  const { root: { nodeId: documentNodeId } } = await DOM.getDocument()\n  const { nodeId } = await DOM.querySelector({\n    selector: selector,\n    nodeId: documentNodeId,\n  })\n\n  const { model } = await DOM.getBoxModel({ nodeId })\n\n  return model\n}\n\nexport function boxModelToViewPort(model: BoxModel, scale: number): Viewport {\n  return {\n    x: model.content[0],\n    y: model.content[1],\n    width: model.width,\n    height: model.height,\n    scale,\n  }\n}\n\nexport async function screenshot(\n  client: Client,\n  selector: string,\n  options: ScreenshotOptions,\n): Promise<string> {\n  const { Page } = client\n\n  const captureScreenshotOptions = {\n    format: 'png',\n    fromSurface: true,\n    clip: undefined,\n  }\n\n  if (selector) {\n    const model = await getBoxModel(client, selector)\n    captureScreenshotOptions.clip = boxModelToViewPort(model, 1)\n  }\n  if (options && options.omitBackground)\n    client.Emulation.setDefaultBackgroundColorOverride({\n      color: { r: 0, g: 0, b: 0, a: 0 },\n    })\n  const screenshot = await Page.captureScreenshot(captureScreenshotOptions)\n  if (options && options.omitBackground)\n    client.Emulation.setDefaultBackgroundColorOverride()\n  return screenshot.data\n}\n\nexport async function html(client: Client): Promise<string> {\n  const { DOM } = client\n\n  const { root: { nodeId } } = await DOM.getDocument()\n  const { outerHTML } = await DOM.getOuterHTML({ nodeId })\n  return outerHTML\n}\n\nexport async function htmlUrl(client: Client): Promise<string> {\n  const { DOM } = client\n\n  const { root: { nodeId } } = await DOM.getDocument()\n  const { outerHTML } = await DOM.getOuterHTML({ nodeId })\n  return outerHTML\n}\n\nexport async function pdf(\n  client: Client,\n  options?: PdfOptions,\n): Promise<string> {\n  const { Page } = client\n\n  const pdf = await Page.printToPDF(options)\n\n  return pdf.data\n}\n\nexport async function clearInput(\n  client: Client,\n  selector: string,\n): Promise<void> {\n  await wait(500)\n  await focus(client, selector)\n\n  const { Input } = client\n\n  const text = await getValue(client, selector)\n\n  const optionsDelete = {\n    nativeVirtualKeyCode: 46,\n    windowsVirtualKeyCode: 46,\n  }\n\n  const optionsBackspace = {\n    nativeVirtualKeyCode: 8,\n    windowsVirtualKeyCode: 8,\n  }\n\n  for (let i = 0; i < text.length; i++) {\n    await Input.dispatchKeyEvent({\n      ...optionsDelete,\n      type: 'rawKeyDown',\n    })\n    Input.dispatchKeyEvent({\n      ...optionsDelete,\n      type: 'keyUp',\n    })\n    await Input.dispatchKeyEvent({\n      ...optionsBackspace,\n      type: 'rawKeyDown',\n    })\n    Input.dispatchKeyEvent({\n      ...optionsBackspace,\n      type: 'keyUp',\n    })\n  }\n}\n\nexport async function setFileInput(\n  client: Client,\n  selector: string,\n  files: string[],\n): Promise<string> {\n  const { DOM } = client\n  const dom = await DOM.getDocument()\n  const node = await DOM.querySelector({\n    nodeId: dom.root.nodeId,\n    selector: selector,\n  })\n  return await DOM.setFileInputFiles({ files: files, nodeId: node.nodeId })\n}\n\nexport function getDebugOption(): boolean {\n  if (\n    process &&\n    process.env &&\n    process.env['DEBUG'] &&\n    process.env['DEBUG'].includes('chromeless')\n  ) {\n    return true\n  }\n\n  return false\n}\n\nexport function writeToFile(\n  data: string,\n  extension: string,\n  filePathOverride: string,\n): string {\n  const filePath =\n    filePathOverride || path.join(os.tmpdir(), `${cuid()}.${extension}`)\n  fs.writeFileSync(filePath, Buffer.from(data, 'base64'))\n  return filePath\n}\n\nfunction getS3BucketName() {\n  return process.env['CHROMELESS_S3_BUCKET_NAME']\n}\n\nfunction getS3BucketUrl() {\n  return process.env['CHROMELESS_S3_BUCKET_URL']\n}\n\nfunction getS3ObjectKeyPrefix() {\n  return process.env['CHROMELESS_S3_OBJECT_KEY_PREFIX'] || ''\n}\n\nfunction getS3FilesPermissions() {\n  return process.env['CHROMELESS_S3_OBJECT_ACL'] || 'public-read'\n}\n\nexport function isS3Configured() {\n  return getS3BucketName() && getS3BucketUrl()\n}\n\nconst s3ContentTypes = {\n  'image/png': {\n    extension: 'png',\n  },\n  'application/pdf': {\n    extension: 'pdf',\n  },\n  'text/html': {\n    extension: 'html',\n  },\n}\n\nexport async function uploadToS3(\n  data: string,\n  contentType: string,\n): Promise<string> {\n  const s3ContentType = s3ContentTypes[contentType]\n  if (!s3ContentType) {\n    throw new Error(`Unknown S3 Content type ${contentType}`)\n  }\n  const s3Path = `${getS3ObjectKeyPrefix()}${cuid()}.${s3ContentType.extension}`\n  const s3 = new AWS.S3()\n  await s3\n    .putObject({\n      Bucket: getS3BucketName(),\n      Key: s3Path,\n      ContentType: contentType,\n      ACL: getS3FilesPermissions(),\n      Body: Buffer.from(data, contentType === 'text/html' ? 'utf8' : 'base64'),\n    })\n    .promise()\n\n  return `https://${getS3BucketUrl()}/${s3Path}`\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es5\",\n    \"moduleResolution\": \"node\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"lib\": [\"es6\", \"dom\"],\n    \"strictNullChecks\": false,\n    \"pretty\": true,\n    \"rootDir\": \".\",\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"exclude\": [\"node_modules\", \"node_modules/**\", \"dist\"],\n  \"include\": [\"./src/**/*.ts\", \"./examples/**/*.ts\"]\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"rules\": {\n    \"class-name\": true,\n    \"comment-format\": [true, \"check-space\"],\n    \"no-var-keyword\": true,\n    \"no-internal-module\": true,\n    \"no-null-keyword\": false,\n    \"prefer-const\": true,\n    \"jsdoc-format\": true\n  }\n}\n"
  }
]