Repository: schickling/chromeless Branch: master Commit: 774223e4e7f6 Files: 40 Total size: 141.0 KB Directory structure: gitextract_s2_draj1/ ├── .circleci/ │ └── config.yml ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs/ │ └── api.md ├── examples/ │ ├── extract-google-results.js │ ├── google-pdf.js │ ├── google-screenshot.js │ ├── mocha-chai-test-example.js │ ├── mouse-event-example.js │ └── twitter.js ├── package.json ├── serverless/ │ ├── README.md │ ├── package.json │ ├── serverless.yml │ ├── src/ │ │ ├── disconnect.ts │ │ ├── run.ts │ │ ├── session.ts │ │ ├── utils.ts │ │ └── version.ts │ └── tsconfig.json ├── src/ │ ├── __tests__/ │ │ └── test.html │ ├── api.ts │ ├── chrome/ │ │ ├── local-runtime.ts │ │ ├── local.ts │ │ └── remote.ts │ ├── index.ts │ ├── queue.ts │ ├── types.ts │ ├── util.test.ts │ └── util.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 workflows: version: 2 chromeless: jobs: - build_node_6 - build_node_8 - release: requires: - build_node_6 - build_node_8 filters: branches: only: master restore_cache: &restore_cache restore_cache: keys: - npm-cache-{{ checksum "package-lock.json" }} save_cache: &save_cache save_cache: key: npm-cache-{{ checksum "package-lock.json" }} paths: - ~/.npm codecov: &codecov run: name: Codecov command: node_modules/.bin/nyc report --reporter=json && bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json jobs: # Build, lint, run tests build_node_6: working_directory: ~/chromeless docker: - image: circleci/node:6 - image: yukinying/chrome-headless-browser steps: - run: node -v; npm -v - checkout - *restore_cache - run: npm install - *save_cache - run: npm test - *codecov build_node_8: working_directory: ~/chromeless docker: - image: circleci/node:8 - image: yukinying/chrome-headless-browser steps: - run: node -v; npm -v - checkout - *restore_cache - run: npm install - *save_cache - run: npm test - *codecov # On master and if tests passed, parse the commit history to see if a new release should happen # If yes, publish to npm and tag a release on GitHub release: working_directory: ~/chromeless docker: - image: circleci/node:8 steps: - run: node -v; npm -v - checkout - *restore_cache - run: npm install - *save_cache - run: npm run build - run: npm run semantic-release ================================================ FILE: .editorconfig ================================================ [*] indent_size = 2 indent_style = space insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true end_of_line = lf ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ # This is a (Bug Report / Feature Proposal) ## Description For bug reports: * What went wrong? * What did you expect should have happened? * What was the config you used? * What stacktrace or error message from your provider did you see? For feature proposals: * What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us. * If there is a new API method, how would it look Similar or dependent issues: * #12345 ## Additional Data * ***Chromeless Version you're using***: * ***Operating System***: * ***Stack Trace***: * ***Error messages***: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ - [ ] If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes) - [ ] Make sure all of the significant new logic is covered by tests - [ ] Rebase your changes on master so that they can be merged easily - [ ] Make sure all tests and linter rules pass - [ ] If you've changed APIs, update the documentation in [README](/) and [/api/README](/api/README.md) ================================================ FILE: .gitignore ================================================ node_modules dist .idea *.log .DS_Store .serverless .build .envrc .nyc_output/ coverage/ yarn.lock ================================================ FILE: .prettierignore ================================================ .nyc_output/ *.md dist/ package.json package-lock.json serverless/ ================================================ FILE: .prettierrc ================================================ { "semi": false, "singleQuote": true, "trailingComma": "all" } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at marco.luethy@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to this project [fork]: https://github.com/graphcool/chromeless/fork [pr]: https://github.com/graphcool/chromeless/compare [code-of-conduct]: CODE_OF_CONDUCT.md Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Contribution Agreement As a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code, you (and, if applicable, your employer) are licensing the submitted code to the open source community subject to the MIT license. ## Submitting a pull request 0. [Fork][fork] and clone the repository 0. Create a new branch: `git checkout -b feature/my-new-feature-name` 0. Run `npm install` to make sure you've got the latest dependencies. 0. Make your change 0. Run the unit tests and make sure they pass and have 100% coverage. (`npm test`) 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Make your commit message follow [Conventional Commits](https://conventionalcommits.org/). - In your pull request description, provide as much detail as possible. This context helps the reviewer to understand the motivation for and impact of the change. - Make sure that all the unit tests still pass. PRs with failing tests won't be merged. ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [GitHub Help](https://help.github.com) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Contributors et.al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ *This project is deprecated in favor for [Puppeteer](https://github.com/GoogleChrome/puppeteer). Thanks to all the contributors who made this project possible.* # Chromeless [![npm](https://img.shields.io/npm/v/chromeless.svg)](https://npmjs.com/package/chromeless) [![downloads](https://img.shields.io/npm/dm/chromeless.svg)](https://npmjs.com/package/chromeless) [![circleci](https://circleci.com/gh/prismagraphql/chromeless.svg?style=shield)](https://circleci.com/gh/prismagraphql/workflows/chromeless/tree/master) [![codecov](https://codecov.io/gh/prismagraphql/chromeless/branch/master/graph/badge.svg)](https://codecov.io/gh/prismagraphql/chromeless) [![dependencies](https://david-dm.org/prismagraphql/chromeless/status.svg)](https://david-dm.org/prismagraphql/chromeless) [![node](https://img.shields.io/node/v/chromeless.svg)]() [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) Chrome automation made simple. Runs locally or headless on AWS Lambda. (**[See Demo](https://chromeless.netlify.com/)**) ## Chromeless can be used to... * Run 1000s of **browser integration tests in parallel** ⚡️ * Crawl the web & automate screenshots * Write bots that require a real browser * *Do pretty much everything you've used __PhantomJS, NightmareJS or Selenium__ for before* ### Examples * [JSON of Google Results](examples/extract-google-results.js): Google for `chromeless` and get a list of JSON results * [Screenshot of Google Results](examples/google-screenshot.js): Google for `chromeless` and take a screenshot of the results * [prep](https://github.com/prismagraphql/prep): Compile-time prerendering for SPA/PWA (like React, Vue...) instead of server-side rendering (SSR) * *See the full [examples list](/examples) for more* ## ▶️ Try it out You can try out Chromeless and explore the API in the browser-based **[demo playground](https://chromeless.netlify.com/)** ([source](https://github.com/prismagraphql/chromeless-playground)). [![](http://i.imgur.com/i1gtCzy.png)](https://chromeless.netlify.com/) ## Contents 1. [How it works](#how-it-works) 1. [Installation](#installation) 1. [Usage](#usage) 1. [API Documentation](#api-documentation) 1. [Configuring Development Environment](#configuring-development-environment) 1. [FAQ](#faq) 1. [Contributors](#contributors) 1. [Credits](#credits) 1. [Help & Community](#help-and-community) ## How it works With Chromeless you can control Chrome (open website, click elements, fill out forms...) using an [elegant API](docs/api.md). This is useful for integration tests or any other scenario where you'd need to script a real browser. ### There are 2 ways to use Chromeless 1. Running Chrome on your local computer 2. Running Chrome on AWS Lambda and controlling it remotely ![](http://imgur.com/2bgTyAi.png) ### 1. Local Setup For local development purposes where a fast feedback loop is necessary, the easiest way to use Chromeless is by controlling your local Chrome browser. Just follow the [usage guide](#usage) to get started. ### 2. Remote Proxy Setup You can also run Chrome in [headless-mode](https://developers.google.com/web/updates/2017/04/headless-chrome) on AWS Lambda. This way you can speed up your tests by running them in parallel. (In [Graphcool](https://www.graph.cool/)'s case this decreased test durations from ~20min to a few seconds.) Chromeless comes out of the box with a remote proxy built-in - the usage stays completely the same. This way you can write and run your tests locally and have them be executed remotely on AWS Lambda. The proxy connects to Lambda through a Websocket connection to forward commands and return the evaluation results. ## Installation ```sh npm install chromeless ``` ### Proxy Setup The project contains a [Serverless](https://serverless.com/) service for running and driving Chrome remotely on AWS Lambda. 1. Deploy The Proxy service to AWS Lambda. More details [here](serverless#setup) 2. Follow the usage instructions [here](serverless#using-the-proxy). ## Usage Using Chromeless is similar to other browser automation tools. For example: ```js const { Chromeless } = require('chromeless') async function run() { const chromeless = new Chromeless() const screenshot = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') .press(13) .wait('#resultStats') .screenshot() console.log(screenshot) // prints local file path or S3 url await chromeless.end() } run().catch(console.error.bind(console)) ``` ### Local Chrome Usage To run Chromeless locally, you need a recent version of Chrome or Chrome Canary installed (version 60 or greater). By default, chromeless will start Chrome automatically and will default to the most recent version found on your system if there's multiple. You can override this behavior by starting Chrome yourself, and passing a flag of `launchChrome: false` in the `Chromeless` constructor. To launch Chrome yourself, and open the port for chromeless, follow this example: ```sh alias canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary" canary --remote-debugging-port=9222 ``` Or run Chrome Canary headless-ly: ```sh canary --remote-debugging-port=9222 --disable-gpu --headless ``` Or run Chrome headless-ly on Windows: ```sh cd "C:\Program Files (x86)\Google\Chrome\Application" chrome --remote-debugging-port=9222 --disable-gpu --headless ``` ### Proxy Usage Follow the setup instructions [here](serverless#installation). Then using Chromeless with the Proxy service is the same as running it locally with the exception of the `remote` option. Alternatively you can configure the Proxy service's endpoint with environment variables. [Here's how](serverless#using-the-proxy). ```js const chromeless = new Chromeless({ remote: { endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev', apiKey: 'your-api-key-here', }, }) ``` ## API Documentation **Chromeless constructor options** - [`new Chromeless(options: ChromelessOptions)`](docs/api.md#chromeless-constructor-options) **Chromeless methods** - [`end()`](docs/api.md#api-end) **Chrome methods** - [`goto(url: string, timeout?: number)`](docs/api.md#api-goto) - [`setUserAgent(useragent: string)`](docs/api.md#api-setUserAgent) - [`click(selector: string, x?: number, y?: number)`](docs/api.md#api-click) - [`wait(timeout: number)`](docs/api.md#api-wait-timeout) - [`wait(selector: string)`](docs/api.md#api-wait-selector) - [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet - [`clearCache()`](docs/api.md#api-clearcache) - [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage) - [`focus(selector: string)`](docs/api.md#api-focus) - [`press(keyCode: number, count?: number, modifiers?: any)`](docs/api.md#api-press) - [`type(input: string, selector?: string)`](docs/api.md#api-type) - [`back()`](docs/api.md#api-back) - Not implemented yet - [`forward()`](docs/api.md#api-forward) - Not implemented yet - [`refresh()`](docs/api.md#api-refresh) - Not implemented yet - [`mousedown(selector: string)`](docs/api.md#api-mousedown) - [`mouseup(selector: string)`](docs/api.md#api-mouseup) - [`scrollTo(x: number, y: number)`](docs/api.md#api-scrollto) - [`scrollToElement(selector: string)`](docs/api.md#api-scrolltoelement) - [`setHtml(html: string)`](docs/api.md#api-sethtml) - [`setExtraHTTPHeaders(headers: Headers)`](docs/api.md#api-setextrahttpheaders) - [`setViewport(options: DeviceMetrics)`](docs/api.md#api-setviewport) - [`evaluate(fn: (...args: any[]) => void, ...args: any[])`](docs/api.md#api-evaluate) - [`inputValue(selector: string)`](docs/api.md#api-inputvalue) - [`exists(selector: string)`](docs/api.md#api-exists) - [`screenshot(selector: string, options: ScreenshotOptions)`](docs/api.md#api-screenshot) - [`pdf(options?: PdfOptions)`](docs/api.md#api-pdf) - [`html()`](docs/api.md#api-html) - [`cookies()`](docs/api.md#api-cookies) - [`cookies(name: string)`](docs/api.md#api-cookies-name) - [`cookies(query: CookieQuery)`](docs/api.md#api-cookies-query) - Not implemented yet - [`allCookies()`](docs/api.md#api-all-cookies) - [`setCookies(name: string, value: string)`](docs/api.md#api-setcookies) - [`setCookies(cookie: Cookie)`](docs/api.md#api-setcookies-one) - [`setCookies(cookies: Cookie[])`](docs/api.md#api-setcookies-many) - [`deleteCookies(name: string)`](docs/api.md#api-deletecookies) - [`clearCookies()`](docs/api.md#api-clearcookies) - [`clearInput(selector: string)`](docs/api.md#api-clearInput) - [`setFileInput(selector: string, files: string | string[])`](docs/api.md#api-set-file-input) ## Configuring Development Environment **Requirements:** - NodeJS version 8.2 and greater 1) Clone this repository 2) Run `npm install` 3) To build: `npm run build` #### Linking this NPM repository 1) Go to this repository locally 2) Run `npm link` 3) Go to the folder housing your chromeless scripts 4) Run `npm link chromeless` Now your local chromeless scripts will use your local development of chromeless. ## FAQ ### How is this different from [NightmareJS](https://github.com/segmentio/nightmare), PhantomJS or Selenium? The `Chromeless` API is very similar to NightmareJS as their API is pretty awesome. The big difference is that `Chromeless` is based on Chrome in [headless-mode](https://developers.google.com/web/updates/2017/04/headless-chrome), and runs in a serverless function in AWS Lambda. The advantage of this is that you can run hundreds of browsers in parallel, without having to think about parallelisation. Running integration Tests for example is much faster. ### I'm new to AWS Lambda, is this still for me? You still can use this locally without Lambda, so yes. Besides that, here is a [simple guide](https://github.com/prismagraphql/chromeless/tree/master/serverless) on how to set the lambda function up for `Chromeless`. ### How much does it cost to run Chromeless in production? > The compute price is $0.00001667 per GB-s and the free tier provides 400,000 GB-s. The request price is $0.20 per 1 million requests and the free tier provides 1M requests per month. This means you can easily execute > 100.000 tests for free in the free tier. ### Are there any limitations? If you're running Chromeless on AWS Lambda, the execution cannot take longer than 5 minutes which is the current limit of Lambda. Besides that, every feature that's supported in Chrome is also working with Chromeless. The maximal number of concurrent function executions is 1000. [AWS API Limits](http://docs.aws.amazon.com/lambda/latest/dg/limits.html) ### Are there commercial options? Although Chromeless is the easiest way to get started running Chrome on Lambda, you may not have time to build and manage your own visual testing toolkit. Commercial options include: * [Chromatic](http://chromaticqa.com): Visual snapshot regression testing for [Storybook](https://storybook.js.org/). ## Troubleshooting ### Error: Unable to get presigned websocket URL and connect to it. In case you get an error like this when running the Chromeless client: ``` { HTTPError: Response code 403 (Forbidden) at stream.catch.then.data (/code/chromeless/node_modules/got/index.js:182:13) at process._tickDomainCallback (internal/process/next_tick.js:129:7) name: 'HTTPError', ... Error: Unable to get presigned websocket URL and connect to it. ``` Make sure that you're running at least version `1.19.0` of [`serverless`](https://github.com/serverless/serverless). It is a known [issue](https://github.com/serverless/serverless/issues/2450), that the API Gateway API keys are not setup correctly in older Serverless versions. Best is to run `npm run deploy` within the project as this will use the local installed version of `serverless`. ### Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev In case the deployment of the serverless function returns an error like this: ``` Serverless Error --------------------------------------- Resource ServerlessDeploymentBucket does not exist for stack chromeless-serverless-dev ``` Please check, that there is no stack with the name `chromeless-serverless-dev` existing yet, otherwise serverless can't correctly provision the bucket. ### No command gets executed In order for the commands to be processed, make sure, that you call one of the commands `screenshot`, `evaluate`, `cookiesGetAll` or `end` at the end of your execution chain. ## Contributors A big thank you to all contributors and supporters of this repository 💚 joelgriffith adieuadieu schickling timsuchanek Chrisgozd criticalbh d2s emeth- githubixx hax Hazealign joeyvandijk liady matthewmueller seangransee sorenbs toddwprice vladgolubev ## Credits * [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface): Chromeless uses this package as an interface to Chrome * [serverless-chrome](https://github.com/adieuadieu/serverless-chrome): Compiled Chrome binary that runs on AWS Lambda (Azure and GCP soon, too.) * [NightmareJS](https://github.com/segmentio/nightmare): We draw a lot of inspiration for the API from this great tool ## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool) Join our [Slack community](http://slack.graph.cool/) if you run into issues or have questions. We love talking to you!

Prisma

================================================ FILE: docs/api.md ================================================ # API Documentation Chromeless provides TypeScript typings. ### Chromeless constructor options `new Chromeless(options: ChromelessOptions)` - `debug: boolean` Show debug output — Default: `false` - `remote: boolean` Use remote chrome process — Default: `false` - `implicitWait: boolean` Wait for element to exist before executing commands — Default: `false` - `waitTimeout: number` Time in ms to wait for element to appear — Default: `10000` - `scrollBeforeClick: boolean` Scroll to element before clicking, usefull if element is outside of viewport — Default: `false` - `viewport: any` Viewport dimensions — Default: `{width: 1440, height: 900, scale: 1}` - `launchChrome: boolean` Auto-launch chrome (local) — Default: `true` - `cdp: CDPOptions` Chome Debugging Protocol Options — Default: `{host: 'localhost', port: 9222, secure: false, closeTab: true}` ### Chromeless methods - [`end()`](#api-end) ### Chrome methods - [`goto(url: string, timeout?: number)`](#api-goto) - [`setUserAgent(useragent: string)`](#api-setuseragent) - [`click(selector: string, x?: number, y?: number)`](#api-click) - [`wait(timeout: number)`](#api-wait-timeout) - [`wait(selector: string, timeout?: number)`](#api-wait-selector) - [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet - [`clearCache()`](#api-clearcache) - [`clearStorage(origin: string, storageTypes: string)`](docs/api.md#api-clearstorage) - [`focus(selector: string)`](#api-focus) - [`press(keyCode: number, count?: number, modifiers?: any)`](#api-press) - [`type(input: string, selector?: string)`](#api-type) - [`back()`](#api-back) - Not implemented yet - [`forward()`](#api-forward) - Not implemented yet - [`refresh()`](#api-refresh) - Not implemented yet - [`mousedown(selector: string)`](#api-mousedown) - [`mouseup(selector: string)`](#api-mouseup) - [`scrollTo(x: number, y: number)`](#api-scrollto) - [`scrollToElement(selector: string)`](#api-scrolltoelement) - [`setHtml(html: string)`](#api-sethtml) - [`setViewport(options: DeviceMetrics)`](#api-setviewport) - [`evaluate(fn: (...args: any[]) => void, ...args: any[])`](#api-evaluate) - [`inputValue(selector: string)`](#api-inputvalue) - [`exists(selector: string)`](#api-exists) - [`screenshot(selector: string, options: ScreenshotOptions)`](#api-screenshot) - [`pdf(options?: PdfOptions)`](#api-pdf) - [`html()`](#api-html) - [`cookies()`](#api-cookies) - [`cookies(name: string)`](#api-cookies-name) - [`cookies(query: CookieQuery)`](#api-cookies-query) - Not implemented yet - [`allCookies()`](#api-all-cookies) - [`setCookies(name: string, value: string)`](#api-setcookies) - [`setCookies(cookie: Cookie)`](#api-setcookies-one) - [`setCookies(cookies: Cookie[])`](#api-setcookies-many) - [`deleteCookies(name: string)`](#api-deletecookies) - [`clearCookies()`](#api-clearcookies) --------------------------------------- ### end(): Promise End the Chromeless session. Locally this will disconnect from Chrome. Over the Proxy, this will end the session, terminating the Lambda function. It returns the last value that has been evaluated. ```js await chromeless.end() ``` --------------------------------------- ### goto(url: string, timeout?: number): Chromeless Navigate to a URL. __Arguments__ - `url` - URL to navigate to - `timeout` -How long to wait for page to load (default is value of waitTimeout option) __Example__ ```js await chromeless.goto('https://google.com/') ``` --------------------------------------- ### setUserAgent(useragent: string): Chromeless Set the useragent of the browser. It should be called before `.goto()`. __Arguments__ - `useragent` - UserAgent to use __Example__ ```js await chromeless.setUserAgent('Custom Chromeless UserAgent x.x.x') ``` --------------------------------------- ### click(selector: string, x?: number, y?: number): Chromeless Click on something in the DOM. __Arguments__ - `selector` - DOM selector for element to click - `x` - Offset from the left of the element, default width/2 - `y` - Offset from the top of the element, default height/2 __Example__ ```js await chromeless.click('#button') await chromeless.click('#button', 20, 100) ``` --------------------------------------- ### wait(timeout: number): Chromeless Wait for some duration. Useful for waiting for things download. __Arguments__ - `timeout` - How long to wait, in ms __Example__ ```js await chromeless.wait(1000) ``` --------------------------------------- ### wait(selector: string, timeout?: number): Chromeless Wait until something appears. Useful for waiting for things to render. __Arguments__ - `selector` - DOM selector to wait for - `timeout` - How long to wait for element to appear (default is value of waitTimeout option) __Example__ ```js await chromeless.wait('div#loaded') await chromeless.wait('div#loaded', 1000) ``` --------------------------------------- ### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless Not implemented yet Wait until a function returns. You can also return some Promise that will be resolved at some point. __Arguments__ - `fn` - Function to wait for - `[arguments]` - Arguments to pass to the function __Example__ ```js await chromeless.wait(() => { return new Promise((resolve, reject) => { // do something async, setTimeout... resolve(); }); }) ``` --------------------------------------- ### clearCache(): Chromeless Clears browser cache. Service workers and Storage (IndexedDB, WebSQL, etc) needs to be cleared separately. More information at the [Chrome Devtools Protocol website](https://chromedevtools.github.io/devtools-protocol/tot). __Example__ ```js await chromeless.clearCache() ``` --------------------------------------- ### clearStorage(origin: string, storageTypes: string): Chromeless Clears browser storage. __Arguments__ - `origin` - Security origin for the storage type we wish to clear - `storageTypes` - A string comma separated list of chrome storage types. Allowed values include: appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage, all, other. More information at the [Chrome Devtools Protocol website](https://chromedevtools.github.io/devtools-protocol/tot/Storage/). __Example__ ```js await chromeless.clearStorage('http://localhost', 'local_storage, websql') await chromeless.clearStorage('*', 'all') ``` --------------------------------------- ### focus(selector: string): Chromeless Provide focus on a DOM element. __Arguments__ - `selector` - DOM selector to focus __Example__ ```js await chromeless.focus('input#searchField') ``` --------------------------------------- ### press(keyCode: number, count?: number, modifiers?: any): Chromeless Send a key press. Enter, for example. __Arguments__ - `keyCode` - Key code to send - `count` - How many times to send the key press - `modifiers` - Modifiers to send along with the press (e.g. control, command, or alt) __Example__ ```js await chromeless.press(13) ``` --------------------------------------- ### type(input: string, selector?: string): Chromeless Type something (into a field, for example). __Arguments__ - `input` - String to type - `selector` - DOM element to type into __Example__ ```js const result = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') ``` --------------------------------------- ### back() - Not implemented yet Not implemented yet --------------------------------------- ### forward() - Not implemented yet Not implemented yet --------------------------------------- ### refresh() - Not implemented yet Not implemented yet --------------------------------------- ### mousedown(selector: string): Chromeless Send mousedown event on something in the DOM. __Arguments__ - `selector` - DOM selector for element to send mousedown event __Example__ ```js await chromeless.mousedown('#item') ``` --------------------------------------- ### mouseup(selector: string): Chromeless Send mouseup event on something in the DOM. __Arguments__ - `selector` - DOM selector for element to send mouseup event __Example__ ```js await chromeless.mouseup('#placeholder') ``` --------------------------------------- ### scrollTo(x: number, y: number): Chromeless Scroll to somewhere in the document. __Arguments__ - `x` - Offset from the left of the document - `y` - Offset from the top of the document __Example__ ```js await chromeless.scrollTo(0, 500) ``` --------------------------------------- ### scrollToElement(selector: string): Chromeless Scroll to location of element. Behavior is simiar to `` — target element will be at the top of viewport __Arguments__ - `selector` - DOM selector for element to scroll to __Example__ ```js await chromeless.scrollToElement('.button') ``` --------------------------------------- ### setHtml(html: string): Chromeless Sets given markup as the document's HTML. __Arguments__ - `html` - HTML to set as the document's markup. __Example__ ```js await chromeless.setHtml('

Hello world!

') ``` ---------------------------------------
### setExtraHTTPHeaders(headers: Headers): Chromeless Sets extra HTTP headers. __Arguments__ - `headers` - headers as keys / values of JSON object __Example__ ```js await chromeless.setExtraHTTPHeaders({ 'accept-language': 'en-US,en;q=0.8' }) ``` --------------------------------------- ### setViewport(options:DeviceMetrics) Resize the viewport. Useful if you want to capture more or less of the document in a screenshot. __Arguments__ - `options` - DeviceMetrics object __Example__ ```js await chromeless.setViewport({width: 1024, height: 600, scale: 1}) ``` --------------------------------------- ### evaluate(fn: (...args: any[]) => void, ...args: any[]): Chromeless Evaluate Javascript code within Chrome in the context of the DOM. Returns the resulting value or a Promise. __Arguments__ - `fn` - Function to evaluate within Chrome, can be async (Promise). - `[arguments]` - Arguments to pass to the function __Example__ ```js await chromeless.evaluate(() => { // this will be executed in Chrome const links = [].map.call( document.querySelectorAll('.g h3 a'), a => ({title: a.innerText, href: a.href}) ) return JSON.stringify(links) }) ``` --------------------------------------- ### inputValue(selector: string): Chromeless Get the value of an input field. __Arguments__ - `selector` - DOM input element __Example__ ```js await chromeless.inputValue('input#searchField') ``` --------------------------------------- ### exists(selector: string): Chromeless Test if a DOM element exists in the document. __Arguments__ - `selector` - DOM element to check for __Example__ ```js await chromeless.exists('div#ready') ``` --------------------------------------- ### screenshot(selector: string, options: ScreenshotOptions): Chromeless Take a screenshot of the document as framed by the viewport or of a specific element (by a selector). When running Chromeless locally this returns the local file path to the screenshot image. When run over the Chromeless Proxy service, a URL to the screenshot on S3 is returned. __Arguments__ - `selector` - DOM element to take a screenshot of, - `options` - An options object with the following props - `options.filePath` - A file path override in case of working locally - `options.omitBackground` - Boolean to remove default white background __Examples__ ```js const screenshot = await chromeless .goto('https://google.com/') .screenshot() console.log(screenshot) // prints local file path or S3 URL ``` ```js const screenshot = await chromeless .goto('https://google.com/') .screenshot('#hplogo', { filePath: path.join(__dirname, 'google-logo.png') }) console.log(screenshot) // prints local file path or S3 URL ``` ```js const screenshot = await chromeless .goto('https://google.com/') .screenshot({ filePath: path.join(__dirname, 'google-search.png') }) console.log(screenshot) // prints local file path or S3 URL ``` --------------------------------------- ### pdf(options?: PdfOptions) - Chromeless Print to a PDF of the document as framed by the viewport. When running Chromeless locally this returns the local file path to the PDF. When run over the Chromeless Proxy service, a URL to the PDF on S3 is returned. Requires that Chrome be running headless-ly. [More](https://github.com/graphcool/chromeless/issues/146) __Arguments__ - `options` - An object containing overrides for [printToPDF() parameters](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF) __Example__ ```js const pdf = await chromeless .goto('https://google.com/') .pdf({landscape: true}) console.log(pdf) // prints local file path or S3 URL ``` --------------------------------------- ### html(): Chromeless Get full HTML of the loaded page. __Example__ ```js const html = await chromeless .setHtml('

Hello world!

') .html() console.log(html) //

Hello world!

``` ---------------------------------------
### cookies(): Chromeless Returns all browser cookies for the current URL. __Example__ ```js await chromeless.cookies() ``` --------------------------------------- ### cookies(name: string): Chromeless Returns a specific browser cookie by name for the current URL. __Arguments__ - `name` - Name of the cookie to get __Example__ ```js const cookie = await chromeless.cookies('creepyTrackingCookie') ``` --------------------------------------- ### cookies(query: CookieQuery) - Not implemented yet Not implemented yet --------------------------------------- ### allCookies(): Chromeless Returns all browser cookies. Nam nom nom. __Example__ ```js await chromeless.allCookies() ``` --------------------------------------- ### setCookies(name: string, value: string): Chromeless Sets a cookie with the given name and value. __Arguments__ - `name` - Name of the cookie - `value` - Value of the cookie __Example__ ```js await chromeless.setCookies('visited', '1') ``` --------------------------------------- ### setCookies(cookie: Cookie): Chromeless Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist. __Arguments__ - `cookie` - The cookie data to set __Example__ ```js await chromeless.setCookies({ url: 'http://google.com/', domain: 'google.com', name: 'userData', value: '{}', path: '/', expires: 0, size: 0, httpOnly: false, secure: true, session: true, }) ``` --------------------------------------- ### setCookies(cookies: Cookie[]): Chromeless Sets many cookies with the given cookie data; may overwrite equivalent cookies if they exist. __Arguments__ - `url` - URL to navigate to __Example__ ```js await chromeless.setCookies([ { url: 'http://google.com/', domain: 'google.com', name: 'userData', value: '{}', path: '/', expires: 0, size: 0, httpOnly: false, secure: true, session: true, }, { url: 'http://bing.com/', domain: 'bing.com', name: 'userData', value: '{}', path: '/', expires: 0, size: 0, httpOnly: false, secure: true, session: true, } ]) ``` --------------------------------------- ### deleteCookies(name: string) - Not implemented yet Delete a specific cookie. __Arguments__ - `name` - name of the cookie __Example__ ```js await chromeless.deleteCookies('cookieName') ``` --------------------------------------- ### clearCookies(): Chromeless Clears all browser cookies. __Example__ ```js await chromeless.clearCookies() ``` --------------------------------------- ### clearInput(selector: string): Chromeless Clear input text. __Example__ ```js await chromeless.clearInput('#username') ``` --------------------------------------- ### setFileInput(selector: string, files: string | string[]): Chromeless Set file(s) for selected file input. Currently not supported in the Proxy. Progress tracked in [#186](https://github.com/graphcool/chromeless/issues/186) __Example__ ```js await chromeless.setFileInput('.uploader', '/User/Me/Documents/img.jpg') ``` ================================================ FILE: examples/extract-google-results.js ================================================ const { Chromeless } = require('chromeless') async function run() { const chromeless = new Chromeless({ remote: true }) const links = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') .press(13) .wait('#resultStats') .evaluate(() => { // this will be executed in headless chrome const links = [].map.call( document.querySelectorAll('.g h3 a'), a => ({title: a.innerText, href: a.href}) ) return JSON.stringify(links) }) // you can still use the method chaining API after evaluating // when you're done, at any time you can call `.then` (in our case `await`) .scrollTo(0, 1000) console.log(links) await chromeless.end() } run().catch(console.error.bind(console)) ================================================ FILE: examples/google-pdf.js ================================================ const { Chromeless } = require('chromeless') async function run() { const chromeless = new Chromeless() const pdf = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') .press(13) .wait('#resultStats') .pdf({ displayHeaderFooter: true, landscape: true }) console.log(pdf) // prints local file path or S3 url await chromeless.end() } run().catch(console.error.bind(console)) ================================================ FILE: examples/google-screenshot.js ================================================ const { Chromeless } = require('chromeless') async function run() { const chromeless = new Chromeless() const screenshot = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') .press(13) .wait('#resultStats') .screenshot() console.log(screenshot) // prints local file path or S3 url await chromeless.end() } run().catch(console.error.bind(console)) ================================================ FILE: examples/mocha-chai-test-example.js ================================================ const { Chromeless } = require('chromeless') const { expect } = require('chai') // make sure you do npm i chai // to run this example just run // mocha path/to/this/file describe('When searching on google', function () { it('shows results', async function () { this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error const chromeless = new Chromeless() await chromeless.goto('https://google.com') .wait('input[name="q"]') .type('chromeless github', 'input[name="q"]') .press(13) // press enter .wait('#resultStats') const result = await chromeless.exists('a[href*="graphcool/chromeless"]') expect(result).to.be.true await chromeless.end() }) }) describe('When clicking on the image of the demo playground', function () { it('should redirect to the demo', async function () { this.timeout(10000); //we need to increase the timeout or else mocha will exit with an error const chromeless = new Chromeless() await chromeless.goto('https://github.com/graphcool/chromeless') .wait('a[href="https://chromeless.netlify.com/"]') .click('a[href="https://chromeless.netlify.com/"]') .wait('#root') const url = await chromeless.evaluate(url => window.location.href) expect(url).to.match(/^https\:\/\/chromeless\.netlify\.com/) await chromeless.end() }) }) ================================================ FILE: examples/mouse-event-example.js ================================================ const { Chromeless } = require('chromeless') async function run() { const chromeless = new Chromeless() const screenshot = await chromeless .goto('https://www.google.com') .mousedown('input[name="btnI"]') .mouseup('input[name="btnI"]') .wait('.latest-doodle') .screenshot() console.log(screenshot) await chromeless.end() } run().catch(console.error.bind(console)) ================================================ FILE: examples/twitter.js ================================================ const { Chromeless } = require('chromeless') const twitterUsername = "xxx" const twitterPassword = "xxx" async function run() { const chromeless = new Chromeless() const screenshot = await chromeless .goto('https://twitter.com/login/') .type(twitterUsername, '.js-username-field') .type(twitterPassword, '.js-password-field') .click('button[type="submit"]') .wait('.status') .screenshot() await chromeless.end() } run().catch(console.error.bind(console)) ================================================ FILE: package.json ================================================ { "name": "chromeless", "version": "1.4.0", "description": "🖥 Chrome automation made simple. Runs locally or headless on AWS Lambda.", "homepage": "https://github.com/graphcool/chromeless", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/graphcool/chromeless.git" }, "bug": { "url": "https://github.com/graphcool/chromeless/issues" }, "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", "files": [ "dist" ], "engines": { "node": ">= 6.10.0" }, "scripts": { "ava": "tsc -d && nyc ava", "build": "npm run clean && tsc -d", "clean": "rimraf dist", "coverage": "npm run ava", "precommit": "lint-staged", "commitmsg": "commitlint -e $GIT_PARAMS", "prettier": "prettier --list-different --write \"**/*.{ts,json}\"", "test": "npm run lint && npm run ava", "lint": "npm run prettier && npm run tslint", "tslint": "tslint -c tslint.json -p tsconfig.json --exclude 'node_modules/**'", "watch": "tsc -w", "watch:test": "tsc -d -w & ava --watch", "semantic-release": "semantic-release" }, "dependencies": { "aws-sdk": "^2.177.0", "bluebird": "^3.5.1", "chrome-launcher": "^0.10.0", "chrome-remote-interface": "^0.25.5", "cuid": "^2.1.0", "form-data": "^2.3.1", "got": "^8.0.0", "mqtt": "^2.15.0" }, "devDependencies": { "@commitlint/config-conventional": "^7.0.0", "@types/bluebird": "^3.5.19", "@types/cuid": "^1.3.0", "@types/node": "^10.0.3", "ava": "^0.25.0", "commitlint": "^7.0.0", "husky": "^0.14.3", "lint-staged": "^7.0.0", "nyc": "^12.0.2", "prettier": "1.11.1", "rimraf": "^2.6.2", "semantic-release": "^15.0.2", "tslint": "^5.8.0", "typescript": "^2.6.2" }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] }, "lint-staged": { "*.{ts}": [ "prettier --parser typescript --no-semi --single-quote --trailing-comma all --write", "tslint", "git add" ], "*.{js}": [ "prettier --no-semi --single-quote --trailing-comma all --write", "lint", "git add" ] } } ================================================ FILE: serverless/README.md ================================================ # Chromeless Proxy service A [Serverless](https://serverless.com/) AWS Lambda service for running and interacting with Chrome remotely with Chromeless. ## Contents 1. [Setup](#setup) 1. [Using the Proxy](#using-the-proxy) ## Setup Clone this repository and enter the `serverless` directory: ```bash git clone https://github.com/graphcool/chromeless.git cd chromeless/serverless npm install ``` ### Configure Next, modify the `custom` section in `serverless.yml`. You must set `awsIotHost` to the your AWS IoT Custom Endpoint for your AWS region. You can find this with the AWS CLI with `aws iot describe-endpoint --output text` or by navigating to the AWS IoT Console and going to Settings. For example: ```yaml ... custom: stage: dev debug: "*" # false if you don't want noise in CloudWatch awsIotHost: ${env:AWS_IOT_HOST} ... ``` You may also need to change the region in the `provider` section in `serverless.yml`: ```yaml ... provider: name: aws runtime: nodejs6.10 stage: ${self:custom.stage} region: YOUR_REGION_HERE ... ``` **Note:** The AWS Lambda function, API Gateway and IoT must all be in the _same_ region. **Note:** Deploying from Windows is currently not supported. See [#70](https://github.com/graphcool/chromeless/issues/70#issuecomment-318634457) ### Credentials Before you can deploy, you must configure your AWS credentials either by defining `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environmental variables, or using an AWS profile. You can read more about this on the [Serverless Credentials Guide](https://serverless.com/framework/docs/providers/aws/guide/credentials/). In short, either: ```bash export AWS_PROFILE= ``` or ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= ``` ### Deploy Once configured, deploying the service can be done with: ```bash npm run deploy ``` Once completed, some service information will be logged. Make note of the `session` GET endpoint and the value of the `dev-chromeless-session-key` API key. You'll need them when using Chromeless through the Proxy. ```log Service Information service: chromeless-serverless stage: dev region: eu-west-1 api keys: dev-chromeless-session-key: X-your-api-key-here-X endpoints: GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/version OPTIONS - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/ GET - https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/ functions: run: chromeless-serverless-dev-run version: chromeless-serverless-dev-version session: chromeless-serverless-dev-session disconnect: chromeless-serverless-dev-disconnect ``` ## Using the Proxy Connect to the proxy service with the `remote` option parameter on the Chromeless constructor. You must provide the endpoint URL provided during deployment either as an argument or set it in the `CHROMELESS_ENDPOINT_URL` environment variable. Note that this endpoint is _different_ from the AWS IoT Custom Endpoint. The Proxy's endpoint URL you want to use will look something like `https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/` ### Option 1: Environment Variables ```bash export CHROMELESS_ENDPOINT_URL=https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev export CHROMELESS_ENDPOINT_API_KEY=your-api-key-here ``` and ```js const chromeless = new Chromeless({ remote: true, }) ``` ### Option 2: Constructor options ```js const chromeless = new Chromeless({ remote: { endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev', apiKey: 'your-api-key-here', }, }) ``` ### Full Example ```js const Chromeless = require('chromeless').default async function run() { const chromeless = new Chromeless({ remote: { endpointUrl: 'https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev', apiKey: 'your-api-key-here' }, }) const screenshot = await chromeless .goto('https://www.google.com') .type('chromeless', 'input[name="q"]') .press(13) .wait('#resultStats') .screenshot() console.log(screenshot) // prints local file path or S3 url await chromeless.end() } run().catch(console.error.bind(console)) ``` ================================================ FILE: serverless/package.json ================================================ { "name": "chromeless-remotechrome-service", "version": "1.3.0", "description": "The Chromeless Proxy AWS Lambda service", "homepage": "https://github.com/graphcool/chromeless", "license": "MIT", "engines": { "node": ">= 6.10.0" }, "scripts": { "deploy": "serverless deploy" }, "dependencies": { "aws4": "^1.6.0", "chromeless": "^1.3.0", "cuid": "^1.3.8", "mqtt": "^2.11.0", "source-map-support": "^0.4.15" }, "devDependencies": { "@types/cuid": "^1.3.0", "@types/node": "^8.0.15", "serverless": "^1.19.0", "serverless-offline": "^3.15.3", "serverless-plugin-chrome": "^1.0.0-11", "serverless-plugin-typescript": "^1.0.0" } } ================================================ FILE: serverless/serverless.yml ================================================ service: chromeless-serverless custom: stage: dev debug: "*" awsIotHost: ${env:AWS_IOT_HOST} chrome: functions: - run provider: name: aws runtime: nodejs6.10 stage: ${self:custom.stage} region: eu-west-1 environment: DEBUG: ${self:custom.debug} AWS_IOT_HOST: ${self:custom.awsIotHost} apiKeys: - ${self:custom.stage}-chromeless-session-key iamRoleStatements: - Effect: "Allow" Action: - "iot:Connect" - "iot:Publish" - "iot:Subscribe" - "iot:Receive" - "iot:GetThingShadow" - "iot:UpdateThingShadow" Resource: "*" - Effect: "Allow" Action: - s3:* Resource: Fn::Join: - "" - - "arn:aws:s3:::" - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -chromeless - /* plugins: - serverless-plugin-typescript - serverless-plugin-chrome - serverless-offline functions: run: memorySize: 1536 timeout: 300 handler: src/run.default events: - iot: sql: "SELECT * FROM 'chrome/new-session'" environment: CHROMELESS_S3_BUCKET_NAME: Fn::Join: - "" - - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -chromeless CHROMELESS_S3_OBJECT_KEY_PREFIX: "" CHROMELESS_S3_OBJECT_ACL: "public-read" CHROMELESS_S3_BUCKET_URL: Fn::GetAtt: - Bucket - DomainName version: memorySize: 128 handler: src/version.default events: - http: path: /version method: GET session: memorySize: 128 timeout: 10 handler: src/session.default events: - http: method: OPTIONS path: / private: true - http: method: GET path: / private: true disconnect: memorySize: 256 handler: src/disconnect.default timeout: 10 events: - iot: sql: "SELECT * FROM 'chrome/last-will'" resources: Resources: RunLogGroup: Properties: RetentionInDays: 7 VersionLogGroup: Properties: RetentionInDays: 7 SessionLogGroup: Properties: RetentionInDays: 7 DisconnectLogGroup: Properties: RetentionInDays: 7 Bucket: Type: AWS::S3::Bucket Properties: BucketName: Fn::Join: - "" - - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -chromeless LifecycleConfiguration: Rules: - ExpirationInDays: 1 Status: Enabled ================================================ FILE: serverless/src/disconnect.ts ================================================ import * as AWS from 'aws-sdk' import { debug } from './utils' const iotData = new AWS.IotData({ endpoint: process.env.AWS_IOT_HOST }) export default async ({ channelId }, context, callback): Promise => { debug('Disconnect on', channelId) let params = { topic: `chrome/${channelId}/end`, payload: JSON.stringify({ channelId, client: true, disconnected: true }), qos: 1, } iotData.publish(params, callback) } ================================================ FILE: serverless/src/run.ts ================================================ import 'source-map-support/register' import { LocalChrome, Queue, ChromelessOptions } from 'chromeless' import { connect as mqtt, MqttClient } from 'mqtt' import { createPresignedURL, debug } from './utils' export default async ( { channelId, options }, context, callback, chromeInstance ): Promise => { // used to block requests from being processed while we're exiting let endingInvocation = false let timeout let executionCheckInterval debug('Invoked with data: ', channelId, options) const chrome = new LocalChrome({ ...options, remote: false, launchChrome: false, cdp: { closeTab: true }, }) const queue = new Queue(chrome) const TOPIC_CONNECTED = `chrome/${channelId}/connected` const TOPIC_REQUEST = `chrome/${channelId}/request` const TOPIC_RESPONSE = `chrome/${channelId}/response` const TOPIC_END = `chrome/${channelId}/end` const channel = mqtt(createPresignedURL()) if (process.env.DEBUG) { channel.on('error', error => debug('WebSocket error', error)) channel.on('offline', () => debug('WebSocket offline')) } /* Clean up function whenever we want to end the invocation. Importantly we publish a message that we're disconnecting, and then we kill the running Chrome instance. */ const end = (topic_end_data = {}) => { if (!endingInvocation) { endingInvocation = true clearInterval(executionCheckInterval) clearTimeout(timeout) channel.unsubscribe(TOPIC_END, () => { channel.publish(TOPIC_END, JSON.stringify({ channelId, chrome: true, ...topic_end_data }), { qos: 0, }, async () => { channel.end() await chrome.close() await chromeInstance.kill() callback() }) }) } } const newTimeout = () => setTimeout(async () => { debug('Timing out. No requests received for 30 seconds.') await end({ inactivity: true }) }, 30000) /* When we're almost out of time, we clean up. Importantly this makes sure that Chrome isn't running on the next invocation and publishes a message to the client letting it know we're disconnecting. */ executionCheckInterval = setInterval(async () => { if (context.getRemainingTimeInMillis() < 5000) { debug('Ran out of execution time.') await end({ outOfTime: true }) } }, 1000) channel.on('connect', () => { debug('Connected to AWS IoT broker') /* Publish that we've connected. This lets the client know that it can start sending requests (commands) for us to process. */ channel.publish(TOPIC_CONNECTED, JSON.stringify({}), { qos: 1 }) /* The main bit. Listen for requests from the client, handle them and respond with the result. */ channel.subscribe(TOPIC_REQUEST, () => { debug(`Subscribed to ${TOPIC_REQUEST}`) timeout = newTimeout() channel.on('message', async (topic, buffer) => { if (TOPIC_REQUEST === topic && !endingInvocation) { const message = buffer.toString() clearTimeout(timeout) debug(`Message from ${TOPIC_REQUEST}`, message) const command = JSON.parse(message) try { const result = await queue.process(command) const remoteResult = JSON.stringify({ value: result, }) debug('Chrome result', result) channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 }) } catch (error) { const remoteResult = JSON.stringify({ error: error.toString(), }) debug('Chrome error', error) channel.publish(TOPIC_RESPONSE, remoteResult, { qos: 1 }) } timeout = newTimeout() } }) }) /* Handle diconnection from the client. Either the client purposfully ended the session, or the client connection was abruptly ended resulting in a last-will message being dispatched by the IoT MQTT broker. */ channel.subscribe(TOPIC_END, async () => { channel.on('message', async (topic, buffer) => { if (TOPIC_END === topic) { const message = buffer.toString() const data = JSON.parse(message) debug(`Message from ${TOPIC_END}`, message) debug( `Client ${data.disconnected ? 'disconnected' : 'ended session'}.` ) await end() debug('Ended successfully.') } }) }) }) } ================================================ FILE: serverless/src/session.ts ================================================ import { connect as mqtt, MqttClient } from 'mqtt' import * as cuid from 'cuid' import { createPresignedURL, debug } from './utils' export default async (event, context, callback): Promise => { const url = createPresignedURL() const channelId = cuid() callback(null, { statusCode: 200, body: JSON.stringify({ url, channelId }), }) } ================================================ FILE: serverless/src/utils.ts ================================================ import * as aws4 from 'aws4' /* This creates a presigned URL for accessing the AWS IoT MQTT Broker. Notably, the sessionToken is simply tacked on to the end, and not signed. Because AWS. Thank you @shortjared for your help pointing this out. */ export function createPresignedURL( { host = process.env.AWS_IOT_HOST, path = '/mqtt', region = process.env.AWS_REGION, service = 'iotdevicegateway', accessKeyId = process.env.AWS_ACCESS_KEY_ID, secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY, sessionToken = process.env.AWS_SESSION_TOKEN, // expires = 0, // @TODO: 300, check if this is working http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html } = {}, ): string { const signed = aws4.sign( { host, path, service, region, signQuery: true, // headers: { // 'X-Amz-Expires': expires, // }, }, { accessKeyId, secretAccessKey, }, ) return `wss://${host}${signed.path}&X-Amz-Security-Token=${encodeURIComponent( sessionToken, )}` } export function debug(...log) { if (process.env.DEBUG) { console.log( ...log.map( argument => typeof argument === 'object' ? JSON.stringify(argument, null, 2) : argument ) ) } } ================================================ FILE: serverless/src/version.ts ================================================ import { version as chromelessVersion } from 'chromeless' const serverlessChromelessVersion = require('../package.json').version export default async (event, context, callback): Promise => { callback(null, { statusCode: 200, body: JSON.stringify({ chromeless: chromelessVersion, serverlessChromeless: serverlessChromelessVersion, }), }) } ================================================ FILE: serverless/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "rootDir": ".", "target": "es6", "sourceMap": true, "moduleResolution": "node" }, "exclude": [ "node_modules" ] } ================================================ FILE: src/__tests__/test.html ================================================ Title

This is a test page for Chromeless unit tests

================================================ FILE: src/api.ts ================================================ import ChromeLocal from './chrome/local' import ChromeRemote from './chrome/remote' import Queue from './queue' import { ChromelessOptions, Headers, Cookie, CookieQuery, PdfOptions, DeviceMetrics, ScreenshotOptions, } from './types' import { getDebugOption } from './util' import { isArray } from 'util' export default class Chromeless implements Promise { private queue: Queue private lastReturnPromise: Promise constructor(options: ChromelessOptions = {}, copyInstance?: Chromeless) { if (copyInstance) { this.queue = copyInstance.queue this.lastReturnPromise = copyInstance.lastReturnPromise return } const mergedOptions: ChromelessOptions = { debug: getDebugOption(), waitTimeout: 10000, remote: false, implicitWait: true, scrollBeforeClick: false, launchChrome: true, ...options, viewport: { scale: 1, ...options.viewport, }, cdp: { host: process.env['CHROMELESS_CHROME_HOST'] || 'localhost', port: parseInt(process.env['CHROMELESS_CHROME_PORT'], 10) || 9222, secure: false, closeTab: true, ...options.cdp, }, } const chrome = mergedOptions.remote ? new ChromeRemote(mergedOptions) : new ChromeLocal(mergedOptions) this.queue = new Queue(chrome) this.lastReturnPromise = Promise.resolve(undefined) } /* * The following 3 members are needed to implement a Promise */ readonly [Symbol.toStringTag]: 'Promise' then( onFulfill?: ((value: T) => U | PromiseLike) | null, onReject?: ((error: any) => U | PromiseLike) | null, ): Promise { return this.lastReturnPromise.then(onFulfill, onReject) as Promise } catch(onrejected?: (reason: any) => U | PromiseLike): Promise { return this.lastReturnPromise.catch(onrejected) as Promise } goto(url: string, timeout?: number): Chromeless { this.queue.enqueue({ type: 'goto', url, timeout }) return this } setUserAgent(useragent: string): Chromeless { this.queue.enqueue({ type: 'setUserAgent', useragent }) return this } click(selector: string, x?: number, y?: number): Chromeless { this.queue.enqueue({ type: 'click', selector, x, y }) return this } wait(timeout: number): Chromeless wait(selector: string, timeout?: number): Chromeless wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless wait(firstArg, ...args: any[]): Chromeless { switch (typeof firstArg) { case 'number': { this.queue.enqueue({ type: 'wait', timeout: firstArg }) break } case 'string': { this.queue.enqueue({ type: 'wait', selector: firstArg, timeout: args[0], }) break } case 'function': { this.queue.enqueue({ type: 'wait', fn: firstArg, args }) break } default: throw new Error(`Invalid wait arguments: ${firstArg} ${args}`) } return this } clearCache(): Chromeless { this.queue.enqueue({ type: 'clearCache' }) return this } clearStorage(origin: string, storageTypes: string): Chromeless { this.queue.enqueue({ type: 'clearStorage', origin, storageTypes }) return this } focus(selector: string): Chromeless { this.queue.enqueue({ type: 'focus', selector }) return this } press(keyCode: number, count?: number, modifiers?: any): Chromeless { this.queue.enqueue({ type: 'press', keyCode, count, modifiers }) return this } type(input: string, selector?: string): Chromeless { this.queue.enqueue({ type: 'type', input, selector }) return this } back(): Chromeless { throw new Error('Not implemented yet') } forward(): Chromeless { throw new Error('Not implemented yet') } refresh(): Chromeless { throw new Error('Not implemented yet') } mousedown(selector: string): Chromeless { this.queue.enqueue({ type: 'mousedown', selector }) return this } mouseup(selector: string): Chromeless { this.queue.enqueue({ type: 'mouseup', selector }) return this } mouseover(): Chromeless { throw new Error('Not implemented yet') } scrollTo(x: number, y: number): Chromeless { this.queue.enqueue({ type: 'scrollTo', x, y }) return this } scrollToElement(selector: string): Chromeless { this.queue.enqueue({ type: 'scrollToElement', selector }) return this } setViewport(options: DeviceMetrics): Chromeless { this.queue.enqueue({ type: 'setViewport', options }) return this } setHtml(html: string): Chromeless { this.queue.enqueue({ type: 'setHtml', html }) return this } setExtraHTTPHeaders(headers: Headers): Chromeless { this.queue.enqueue({ type: 'setExtraHTTPHeaders', headers }) return this } evaluate( fn: (...args: any[]) => U, ...args: any[] ): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnCode', fn: fn.toString(), args, }) return new Chromeless({}, this) } inputValue(selector: string): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnInputValue', selector, }) return new Chromeless({}, this) } exists(selector: string): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnExists', selector, }) return new Chromeless({}, this) } screenshot( selector?: string, options?: ScreenshotOptions, ): Chromeless { if (typeof selector === 'object') { options = selector selector = undefined } this.lastReturnPromise = this.queue.process({ type: 'returnScreenshot', selector, options, }) return new Chromeless({}, this) } html(): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnHtml' }) return new Chromeless({}, this) } htmlUrl(): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnHtmlUrl', }) return new Chromeless({}, this) } pdf(options?: PdfOptions): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'returnPdf', options, }) return new Chromeless({}, this) } /** * Get the cookies for the current url */ cookies(): Chromeless /** * Get a specific cookie for the current url * @param name */ cookies(name: string): Chromeless /** * Get a specific cookie by query. Not implemented yet * @param query */ cookies(query: CookieQuery): Chromeless cookies( nameOrQuery?: string | CookieQuery, ): Chromeless { if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery !== 'string') { throw new Error('Querying cookies is not implemented yet') } this.lastReturnPromise = this.queue.process({ type: 'cookies', nameOrQuery, }) return new Chromeless({}, this) } allCookies(): Chromeless { this.lastReturnPromise = this.queue.process({ type: 'allCookies', }) return new Chromeless({}, this) } setCookies(name: string, value: string): Chromeless setCookies(cookie: Cookie): Chromeless setCookies(cookies: Cookie[]): Chromeless setCookies(nameOrCookies, value?: string): Chromeless { this.queue.enqueue({ type: 'setCookies', nameOrCookies, value }) return this } deleteCookies(name: string, url: string): Chromeless { if (typeof name === 'undefined') { throw new Error('Cookie name should be defined.') } if (typeof url === 'undefined') { throw new Error('Cookie url should be defined.') } this.queue.enqueue({ type: 'deleteCookies', name, url }) return this } clearCookies(): Chromeless { this.queue.enqueue({ type: 'clearCookies' }) return this } clearInput(selector: string): Chromeless { this.queue.enqueue({ type: 'clearInput', selector }) return this } setFileInput(selector: string, files: string): Chromeless setFileInput(selector: string, files: string[]): Chromeless setFileInput(selector: string, files: string | string[]): Chromeless { if (!isArray(files)) { files = [files] } this.queue.enqueue({ type: 'setFileInput', selector, files }) return this } async end(): Promise { const result = await this.lastReturnPromise await this.queue.end() return result } } ================================================ FILE: src/chrome/local-runtime.ts ================================================ import { Client, Command, ChromelessOptions, Headers, Cookie, CookieQuery, PdfOptions, ScreenshotOptions, } from '../types' import { nodeExists, wait, waitForNode, click, evaluate, screenshot, html, htmlUrl, pdf, type, getValue, scrollTo, scrollToElement, setHtml, setExtraHTTPHeaders, press, setViewport, clearCookies, deleteCookie, getCookies, setCookies, getAllCookies, version, mousedown, mouseup, focus, clearInput, setFileInput, writeToFile, isS3Configured, uploadToS3, eventToPromise, waitForPromise, } from '../util' export default class LocalRuntime { private client: Client private chromelessOptions: ChromelessOptions private userAgentValue: string constructor(client: Client, chromelessOptions: ChromelessOptions) { this.client = client this.chromelessOptions = chromelessOptions } async run(command: Command): Promise { switch (command.type) { case 'goto': return this.goto(command.url, command.timeout) case 'setViewport': return setViewport(this.client, command.options) case 'wait': { if (command.selector) { return this.waitSelector(command.selector, command.timeout) } else if (command.timeout) { return this.waitTimeout(command.timeout) } else { throw new Error('waitFn not yet implemented') } } case 'clearCache': return this.clearCache() case 'clearStorage': return this.clearStorage(command.origin, command.storageTypes) case 'setUserAgent': return this.setUserAgent(command.useragent) case 'click': return this.click(command.selector, command.x, command.y) case 'returnCode': return this.returnCode(command.fn, ...command.args) case 'returnExists': return this.returnExists(command.selector) case 'returnScreenshot': return this.returnScreenshot(command.selector, command.options) case 'returnHtml': return this.returnHtml() case 'returnHtmlUrl': return this.returnHtmlUrl() case 'returnPdf': return this.returnPdf(command.options) case 'returnInputValue': return this.returnInputValue(command.selector) case 'type': return this.type(command.input, command.selector) case 'press': return this.press(command.keyCode, command.count, command.modifiers) case 'scrollTo': return this.scrollTo(command.x, command.y) case 'scrollToElement': return this.scrollToElement(command.selector) case 'deleteCookies': return this.deleteCookies(command.name, command.url) case 'clearCookies': return this.clearCookies() case 'setHtml': return this.setHtml(command.html) case 'setExtraHTTPHeaders': return this.setExtraHTTPHeaders(command.headers) case 'cookies': return this.cookies(command.nameOrQuery) case 'allCookies': return this.allCookies() case 'setCookies': return this.setCookies(command.nameOrCookies, command.value) case 'mousedown': return this.mousedown(command.selector) case 'mouseup': return this.mouseup(command.selector) case 'focus': return this.focus(command.selector) case 'clearInput': return this.clearInput(command.selector) case 'setFileInput': return this.setFileInput(command.selector, command.files) default: throw new Error(`No such command: ${JSON.stringify(command)}`) } } private async goto( url: string, waitTimeout: number = this.chromelessOptions.waitTimeout, ): Promise { const { Network, Page } = this.client await Promise.all([Network.enable(), Page.enable()]) if (!this.userAgentValue) this.userAgentValue = `Chromeless ${version}` await Network.setUserAgentOverride({ userAgent: this.userAgentValue }) const e2p = eventToPromise() Page.loadEventFired(e2p.onEvent) await Page.navigate({ url }) await waitForPromise(e2p.fired(), waitTimeout, 'page load event') this.log(`Navigated to ${url}`) } private async clearCache(): Promise { const { Network } = this.client const canClearCache = await Network.canClearBrowserCache if (canClearCache) { await Network.clearBrowserCache() this.log(`Cache is cleared`) } else { this.log(`Cache could not be cleared`) } } private async clearStorage( origin: string, storageTypes: string, ): Promise { const { Storage, Network } = this.client const canClearCache = await Network.canClearBrowserCache if (canClearCache) { await Storage.clearDataForOrigin({ origin, storageTypes }) this.log(`${storageTypes} for ${origin} is cleared`) } else { this.log(`${storageTypes} could not be cleared`) } } private async setUserAgent(useragent: string): Promise { this.userAgentValue = useragent await this.log(`Set useragent to ${this.userAgentValue}`) } private async waitTimeout(timeout: number): Promise { this.log(`Waiting for ${timeout}ms`) await wait(timeout) } private async waitSelector( selector: string, waitTimeout: number = this.chromelessOptions.waitTimeout, ): Promise { this.log(`Waiting for ${selector} ${waitTimeout}`) await waitForNode(this.client, selector, waitTimeout) this.log(`Waited for ${selector}`) } private async click(selector: string, x?: number, y?: number): Promise { if (this.chromelessOptions.implicitWait) { this.log(`click(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error(`click(): node for selector ${selector} doesn't exist`) } const { scale } = this.chromelessOptions.viewport if (this.chromelessOptions.scrollBeforeClick) { await scrollToElement(this.client, selector) } await click(this.client, selector, scale, x, y) this.log(`Clicked on ${selector} at (${x}, ${y})`) } private async returnCode(fn: string, ...args: any[]): Promise { return (await evaluate(this.client, fn, ...args)) as T } private async scrollTo(x: number, y: number): Promise { return scrollTo(this.client, x, y) } private async scrollToElement(selector: string): Promise { if (this.chromelessOptions.implicitWait) { this.log(`scrollToElement(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } return scrollToElement(this.client, selector) } private async mousedown(selector: string): Promise { if (this.chromelessOptions.implicitWait) { this.log(`mousedown(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error( `mousedown(): node for selector ${selector} doesn't exist`, ) } const { scale } = this.chromelessOptions.viewport await mousedown(this.client, selector, scale) this.log(`Mousedown on ${selector}`) } private async mouseup(selector: string): Promise { if (this.chromelessOptions.implicitWait) { this.log(`mouseup(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error(`mouseup(): node for selector ${selector} doesn't exist`) } const { scale } = this.chromelessOptions.viewport await mouseup(this.client, selector, scale) this.log(`Mouseup on ${selector}`) } private async setHtml(html: string): Promise { await setHtml(this.client, html) } private async focus(selector: string): Promise { if (this.chromelessOptions.implicitWait) { this.log(`focus(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error(`focus(): node for selector ${selector} doesn't exist`) } await focus(this.client, selector) this.log(`Focus on ${selector}`) } async type(text: string, selector?: string): Promise { if (selector) { if (this.chromelessOptions.implicitWait) { this.log(`type(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error(`type(): Node not found for selector: ${selector}`) } } await type(this.client, text, selector) this.log(`Typed ${text} in ${selector}`) } async cookies(nameOrQuery?: string | CookieQuery): Promise { return await getCookies(this.client, nameOrQuery as string | undefined) } async allCookies(): Promise { return await getAllCookies(this.client) } async setExtraHTTPHeaders(headers: Headers): Promise { return await setExtraHTTPHeaders(this.client, headers) } async setCookies( nameOrCookies: string | Cookie | Cookie[], value?: string, ): Promise { if (typeof nameOrCookies !== 'string' && !value) { const cookies = Array.isArray(nameOrCookies) ? nameOrCookies : [nameOrCookies] return await setCookies(this.client, cookies) } if (typeof nameOrCookies === 'string' && typeof value === 'string') { const fn = () => location.href const url = (await evaluate(this.client, `${fn}`)) as string const cookie: Cookie = { url, name: nameOrCookies, value, } return await setCookies(this.client, [cookie]) } throw new Error(`setCookies(): Invalid input ${nameOrCookies}, ${value}`) } async deleteCookies(name: string, url: string): Promise { const { Network } = this.client const canClearCookies = await Network.canClearBrowserCookies() if (canClearCookies) { await deleteCookie(this.client, name, url) this.log(`Cookie ${name} cleared`) } else { this.log(`Cookie ${name} could not be cleared`) } } async clearCookies(): Promise { const { Network } = this.client const canClearCookies = await Network.canClearBrowserCookies() if (canClearCookies) { await clearCookies(this.client) this.log('Cookies cleared') } else { this.log('Cookies could not be cleared') } } async press(keyCode: number, count?: number, modifiers?: any): Promise { this.log(`Sending keyCode ${keyCode} (modifiers: ${modifiers})`) await press(this.client, keyCode, count, modifiers) } async returnExists(selector: string): Promise { return await nodeExists(this.client, selector) } async returnInputValue(selector: string): Promise { const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error(`value: node for selector ${selector} doesn't exist`) } return getValue(this.client, selector) } // Returns the S3 url or local file path async returnScreenshot( selector?: string, options?: ScreenshotOptions, ): Promise { if (selector) { if (this.chromelessOptions.implicitWait) { this.log(`screenshot(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error( `screenshot(): node for selector ${selector} doesn't exist`, ) } } const data = await screenshot(this.client, selector, options) if (isS3Configured()) { return await uploadToS3(data, 'image/png') } else { return writeToFile(data, 'png', options && options.filePath) } } async returnHtml(): Promise { return await html(this.client) } async returnHtmlUrl(options?: { filePath?: string }): Promise { const data = await html(this.client) if (isS3Configured()) { return await uploadToS3(data, 'text/html') } else { return writeToFile(data, 'html', options && options.filePath) } } // Returns the S3 url or local file path async returnPdf(options?: PdfOptions): Promise { const { filePath, ...cdpOptions } = options || { filePath: undefined } const data = await pdf(this.client, cdpOptions) if (isS3Configured()) { return await uploadToS3(data, 'application/pdf') } else { return writeToFile(data, 'pdf', filePath) } } async clearInput(selector: string): Promise { if (selector) { if (this.chromelessOptions.implicitWait) { this.log(`clearInput(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error( `clearInput(): Node not found for selector: ${selector}`, ) } } await clearInput(this.client, selector) this.log(`${selector} cleared`) } async setFileInput(selector: string, files: string[]): Promise { if (this.chromelessOptions.implicitWait) { this.log(`setFileInput(): Waiting for ${selector}`) await waitForNode( this.client, selector, this.chromelessOptions.waitTimeout, ) } const exists = await nodeExists(this.client, selector) if (!exists) { throw new Error( `setFileInput(): node for selector ${selector} doesn't exist`, ) } await setFileInput(this.client, selector, files) this.log(`setFileInput() files ${files}`) } private log(msg: string): void { if (this.chromelessOptions.debug) { console.log(msg) } } } ================================================ FILE: src/chrome/local.ts ================================================ import { Chrome, Command, ChromelessOptions, Client } from '../types' import * as CDP from 'chrome-remote-interface' import { LaunchedChrome, launch } from 'chrome-launcher' import LocalRuntime from './local-runtime' import { evaluate, setViewport } from '../util' import { DeviceMetrics } from '../types' interface RuntimeClient { client: Client runtime: LocalRuntime } export default class LocalChrome implements Chrome { private options: ChromelessOptions private runtimeClientPromise: Promise private chromeInstance?: LaunchedChrome constructor(options: ChromelessOptions = {}) { this.options = options this.runtimeClientPromise = this.initRuntimeClient() } private async initRuntimeClient(): Promise { const client = this.options.launchChrome ? await this.startChrome() : await this.connectToChrome() const { viewport = {} as DeviceMetrics } = this.options await setViewport(client, viewport as DeviceMetrics) const runtime = new LocalRuntime(client, this.options) return { client, runtime } } private async startChrome(): Promise { const { port } = this.options.cdp this.chromeInstance = await launch({ logLevel: this.options.debug ? 'info' : 'silent', chromeFlags: [ // Do not render scroll bars '--hide-scrollbars', // The following options copied verbatim from https://github.com/GoogleChrome/chrome-launcher/blob/master/src/flags.ts // Disable built-in Google Translate service '--disable-translate', // Disable all chrome extensions entirely '--disable-extensions', // Disable various background network services, including extension updating, // safe browsing service, upgrade detector, translate, UMA '--disable-background-networking', // Disable fetching safebrowsing lists, likely redundant due to disable-background-networking '--safebrowsing-disable-auto-update', // Disable syncing to a Google account '--disable-sync', // Disable reporting to UMA, but allows for collection '--metrics-recording-only', // Mute any audio '--mute-audio', // Skip first run wizards '--no-first-run', ], port, }) const target = await CDP.New({ port, }) return await CDP({ target, port }) } private async connectToChrome(): Promise { const { host, port } = this.options.cdp const target = await CDP.New({ port, host, }) return await CDP({ target, host, port }) } private async setViewport(client: Client) { const { viewport = {} } = this.options const config: any = { deviceScaleFactor: 1, mobile: false, scale: viewport.scale || 1, fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable } const { host, port } = this.options.cdp const versionResult = await CDP.Version({ host, port }) const isHeadless = versionResult['User-Agent'].includes('Headless') if (viewport.height && viewport.width) { config.height = viewport.height config.width = viewport.width } else if (isHeadless) { // just apply default value in headless mode to maintain original browser viewport config.height = 900 config.width = 1440 } else { config.height = await evaluate( client, (() => window.innerHeight).toString(), ) config.width = await evaluate( client, (() => window.innerWidth).toString(), ) } await client.Emulation.setDeviceMetricsOverride(config) await client.Emulation.setVisibleSize({ width: config.width, height: config.height, }) } async process(command: Command): Promise { const { runtime } = await this.runtimeClientPromise return (await runtime.run(command)) as T } async close(): Promise { const { client } = await this.runtimeClientPromise if (this.options.cdp.closeTab) { const { host, port } = this.options.cdp await CDP.Close({ host, port, id: client.target.id }) } if (this.chromeInstance) { this.chromeInstance.kill() } await client.close() } } ================================================ FILE: src/chrome/remote.ts ================================================ import { Chrome, ChromelessOptions, Command, RemoteOptions } from '../types' import { connect as mqtt, MqttClient } from 'mqtt' import * as cuid from 'cuid' import * as got from 'got' interface RemoteResult { value?: any error?: string } function getEndpoint(remoteOptions: RemoteOptions | boolean): RemoteOptions { if (typeof remoteOptions === 'object' && remoteOptions.endpointUrl) { return remoteOptions } if ( process.env['CHROMELESS_ENDPOINT_URL'] && process.env['CHROMELESS_ENDPOINT_API_KEY'] ) { return { endpointUrl: process.env['CHROMELESS_ENDPOINT_URL'], apiKey: process.env['CHROMELESS_ENDPOINT_API_KEY'], } } throw new Error( 'No Chromeless remote endpoint & API key provided. Either set as "remote" option in constructor or set as "CHROMELESS_ENDPOINT_URL" and "CHROMELESS_ENDPOINT_API_KEY" env variables.', ) } export default class RemoteChrome implements Chrome { private options: ChromelessOptions private channelId: string private channel: MqttClient private connectionPromise: Promise private TOPIC_NEW_SESSION: string private TOPIC_CONNECTED: string private TOPIC_REQUEST: string private TOPIC_RESPONSE: string private TOPIC_END: string constructor(options: ChromelessOptions) { this.options = options this.connectionPromise = this.initConnection() } private async initConnection(): Promise { await new Promise(async (resolve, reject) => { const timeout = setTimeout(() => { if (this.channel) { this.channel.end() } reject( new Error( "Timed out after 30sec. Connection couldn't be established.", ), ) }, 30000) try { const { endpointUrl, apiKey } = getEndpoint(this.options.remote) const { body: { url, channelId } } = await got(endpointUrl, { headers: apiKey ? { 'x-api-key': apiKey, } : undefined, json: true, }) this.channelId = channelId this.TOPIC_NEW_SESSION = 'chrome/new-session' this.TOPIC_CONNECTED = `chrome/${channelId}/connected` this.TOPIC_REQUEST = `chrome/${channelId}/request` this.TOPIC_RESPONSE = `chrome/${channelId}/response` this.TOPIC_END = `chrome/${channelId}/end` const channel = mqtt(url, { will: { topic: 'chrome/last-will', payload: JSON.stringify({ channelId }), qos: 1, retain: false, }, }) this.channel = channel if (this.options.debug) { channel.on('error', error => console.log('WebSocket error', error)) channel.on('offline', () => console.log('WebSocket offline')) } channel.on('connect', () => { if (this.options.debug) { console.log('Connected to message broker.') } channel.subscribe(this.TOPIC_CONNECTED, { qos: 1 }, () => { channel.on('message', async topic => { if (this.TOPIC_CONNECTED === topic) { clearTimeout(timeout) resolve() } }) channel.publish( this.TOPIC_NEW_SESSION, JSON.stringify({ channelId, options: this.options }), { qos: 1 }, ) }) channel.subscribe(this.TOPIC_END, () => { channel.on('message', async (topic, buffer) => { if (this.TOPIC_END === topic) { const message = buffer.toString() const data = JSON.parse(message) if (data.outOfTime) { console.warn( `Chromeless Proxy disconnected because it reached it's execution time limit (5 minutes).`, ) } else if (data.inactivity) { console.warn( 'Chromeless Proxy disconnected due to inactivity (no commands sent for 30 seconds).', ) } else { console.warn( `Chromeless Proxy disconnected (we don't know why).`, data, ) } await this.close() } }) }) }) } catch (error) { console.error(error) reject( new Error('Unable to get presigned websocket URL and connect to it.'), ) } }) } async process(command: Command): Promise { // wait until lambda connection is established await this.connectionPromise if (this.options.debug) { console.log(`Running remotely: ${JSON.stringify(command)}`) } const promise = new Promise((resolve, reject) => { this.channel.subscribe(this.TOPIC_RESPONSE, () => { this.channel.on('message', (topic, buffer) => { if (this.TOPIC_RESPONSE === topic) { const message = buffer.toString() const result = JSON.parse(message) as RemoteResult if (result.error) { reject(result.error) } else if (result.value) { resolve(result.value) } else { resolve() } } }) this.channel.publish(this.TOPIC_REQUEST, JSON.stringify(command)) }) }) return promise } async close(): Promise { this.channel.publish( this.TOPIC_END, JSON.stringify({ channelId: this.channelId, client: true }), ) this.channel.end() } } ================================================ FILE: src/index.ts ================================================ import Chromeless from './api' import Queue from './queue' import LocalChrome from './chrome/local' import { version } from './util' import { Cookie, ChromelessOptions } from './types' export { Queue, LocalChrome, Chromeless, Cookie, ChromelessOptions, version } export default Chromeless ================================================ FILE: src/queue.ts ================================================ import { Chrome, Command } from './types' export default class Queue { private flushCount: number private commandQueue: { [flushCount: number]: Command[] } private chrome: Chrome private lastWaitAll: Promise constructor(chrome: Chrome) { this.chrome = chrome this.flushCount = 0 this.commandQueue = { 0: [], } } async end(): Promise { this.lastWaitAll = this.waitAll() await this.lastWaitAll await this.chrome.close() } enqueue(command: Command): void { this.commandQueue[this.flushCount].push(command) } async process(command: Command): Promise { // with lastWaitAll we build a promise chain // already change the pointer to lastWaitAll for the next .process() call // after the pointer is set, wait for the previous tasks // then wait for the own pointer (the new .lastWaitAll) if (this.lastWaitAll) { const lastWaitAllTmp = this.lastWaitAll this.lastWaitAll = this.waitAll() await lastWaitAllTmp } else { this.lastWaitAll = this.waitAll() } await this.lastWaitAll return this.chrome.process(command) } private async waitAll(): Promise { const previousFlushCount = this.flushCount this.flushCount++ this.commandQueue[this.flushCount] = [] for (const command of this.commandQueue[previousFlushCount]) { await this.chrome.process(command) } } } ================================================ FILE: src/types.ts ================================================ export interface Client { Network: any Page: any DOM: any Input: any Runtime: any Emulation: any Storage: any close: () => void target: { id: string } port: any host: any } export interface DeviceMetrics { width: number height: number deviceScaleFactor?: number mobile?: boolean scale?: number screenOrientation?: ScreenOrientation } export interface ScreenOrientation { type: string angle: number } export interface RemoteOptions { endpointUrl: string apiKey?: string } export interface CDPOptions { host?: string // localhost port?: number // 9222 secure?: boolean // false closeTab?: boolean // true } export interface ChromelessOptions { debug?: boolean // false waitTimeout?: number // 10000ms implicitWait?: boolean // false scrollBeforeClick?: boolean // false viewport?: { width?: number // 1440 if headless height?: number // 900 if headless scale?: number // 1 } launchChrome?: boolean // auto-launch chrome (local) `true` cdp?: CDPOptions remote?: RemoteOptions | boolean } export interface Chrome { process(command: Command): Promise close(): Promise } export type Command = | { type: 'goto' url: string timeout?: number } | { type: 'clearCache' } | { type: 'setViewport' options: DeviceMetrics } | { type: 'setUserAgent' useragent: string } | { type: 'wait' timeout?: number selector?: string fn?: string args?: any[] } | { type: 'click' selector: string x?: number y?: number } | { type: 'returnCode' fn: string args?: any[] } | { type: 'returnInputValue' selector: string } | { type: 'returnExists' selector: string } | { type: 'returnValue' selector: string } | { type: 'returnScreenshot' selector?: string options?: ScreenshotOptions } | { type: 'returnHtml' } | { type: 'returnHtmlUrl' } | { type: 'returnPdf' options?: PdfOptions } | { type: 'scrollTo' x: number y: number } | { type: 'scrollToElement' selector: string } | { type: 'setHtml' html: string } | { type: 'setExtraHTTPHeaders' headers: Headers } | { type: 'press' keyCode: number count?: number modifiers?: any } | { type: 'type' input: string selector?: string } | { type: 'clearCookies' } | { type: 'clearStorage' origin: string storageTypes: string } | { type: 'deleteCookies' name: string url: string } | { type: 'setCookies' nameOrCookies: string | Cookie | Cookie[] value?: string } | { type: 'allCookies' } | { type: 'cookies' nameOrQuery?: string | CookieQuery } | { type: 'mousedown' selector: string } | { type: 'mouseup' selector: string } | { type: 'focus' selector: string } | { type: 'clearInput' selector: string } | { type: 'setFileInput' selector: string files: string[] } export type Headers = Record export interface Cookie { url?: string domain?: string name: string value: string path?: string expires?: number size?: number httpOnly?: boolean secure?: boolean session?: boolean } export interface CookieQuery { name: string path?: string expires?: number size?: number httpOnly?: boolean secure?: boolean session?: boolean } // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF export interface PdfOptions { landscape?: boolean displayHeaderFooter?: boolean printBackground?: boolean scale?: number paperWidth?: number paperHeight?: number marginTop?: number marginBottom?: number marginLeft?: number marginRight?: number pageRanges?: string ignoreInvalidPageRanges?: boolean filePath?: string // for internal use } export interface ScreenshotOptions { filePath?: string omitBackground?: boolean } export type Quad = Array export interface ShapeOutsideInfo { bounds: Quad shape: Array marginShape: Array } export interface BoxModel { content: Quad padding: Quad border: Quad margin: Quad width: number height: number shapeOutside: ShapeOutsideInfo } export interface Viewport { x: number y: number width: number height: number scale: number } ================================================ FILE: src/util.test.ts ================================================ import * as fs from 'fs' import * as os from 'os' import * as CDP from 'chrome-remote-interface' import test from 'ava' import Chromeless from '../src' const testHtml = fs.readFileSync('./src/__tests__/test.html') const testUrl = `data:text/html,${testHtml}` const getPngMetaData = async (filePath): Promise => { const fd = fs.openSync(filePath, 'r') return await new Promise(resolve => { fs.read(fd, Buffer.alloc(24), 0, 24, 0, (err, bytesRead, buffer) => resolve({ width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20), }), ) }) } // POC test('evaluate (document.title)', async t => { const chromeless = new Chromeless({ launchChrome: false }) const title = await chromeless.goto(testUrl).evaluate(() => document.title) await chromeless.end() t.is(title, 'Title') }) test('screenshot and pdf path', async t => { const chromeless = new Chromeless({ launchChrome: false }) const screenshot = await chromeless.goto(testUrl).screenshot() const pdf = await chromeless.goto(testUrl).pdf() await chromeless.end() const regex = new RegExp(os.tmpdir().replace(/\\/g, '\\\\')) t.regex(screenshot, regex) t.regex(pdf, regex) }) test('screenshot by selector', async t => { const version = await CDP.Version() const versionMajor = parseInt(/Chrome\/(\d+)/.exec(version['User-Agent'])[1]) // clipping will only work on chrome 61+ const chromeless = new Chromeless({ launchChrome: false }) const screenshot = await chromeless.goto(testUrl).screenshot('img') await chromeless.end() const png = await getPngMetaData(screenshot) t.is(png.width, versionMajor > 60 ? 512 : 1440) t.is(png.height, versionMajor > 60 ? 512 : 900) }) ================================================ FILE: src/util.ts ================================================ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import * as cuid from 'cuid' import { Client, Cookie, DeviceMetrics, PdfOptions, BoxModel, Viewport, Headers, ScreenshotOptions, } from './types' import * as CDP from 'chrome-remote-interface' import * as AWS from 'aws-sdk' export const version: string = ((): string => { if (fs.existsSync(path.join(__dirname, '../package.json'))) { // development (look in /src) return require('../package.json').version } else { // production (look in /dist/src) return require('../../package.json').version } })() export async function setViewport( client: Client, viewport: DeviceMetrics = { width: 1, height: 1, scale: 1 }, ): Promise { const config: any = { deviceScaleFactor: 1, mobile: false, scale: viewport.scale || 1, fitWindow: false, // as we cannot resize the window, `fitWindow: false` is needed in order for the viewport to be resizable } const { host, port } = client const versionResult = await CDP.Version({ host, port }) const isHeadless = versionResult['User-Agent'].includes('Headless') if (viewport.height && viewport.width) { config.height = viewport.height config.width = viewport.width } else if (isHeadless) { // just apply default value in headless mode to maintain original browser viewport config.height = 900 config.width = 1440 } else { config.height = await evaluate( client, (() => window.innerHeight).toString(), ) config.width = await evaluate(client, (() => window.innerWidth).toString()) } await client.Emulation.setDeviceMetricsOverride(config) await client.Emulation.setVisibleSize({ width: config.width, height: config.height, }) return } export async function waitForNode( client: Client, selector: string, waitTimeout: number, ): Promise { const { Runtime } = client const getNode = `selector => { return document.querySelector(selector) }` const result = await Runtime.evaluate({ expression: `(${getNode})(\`${selector}\`)`, }) if (result.result.value === null) { const start = new Date().getTime() return new Promise((resolve, reject) => { const interval = setInterval(async () => { if (new Date().getTime() - start > waitTimeout) { clearInterval(interval) reject( new Error(`wait("${selector}") timed out after ${waitTimeout}ms`), ) } const result = await Runtime.evaluate({ expression: `(${getNode})(\`${selector}\`)`, }) if (result.result.value !== null) { clearInterval(interval) resolve() } }, 500) }) } else { return } } export async function wait(timeout: number): Promise { return new Promise((resolve, reject) => setTimeout(resolve, timeout)) } export async function waitForPromise( promise: Promise, waitTimeout: number, label?: string, ): Promise { return new Promise((resolve, reject) => { let fullfilled = false setTimeout(() => { fullfilled = true reject( new Error( `wait(${label || 'Promise'}) timed out after ${waitTimeout}ms`, ), ) }, waitTimeout) return promise .then(res => (fullfilled ? void 0 : resolve(res))) .catch(err => (fullfilled ? void 0 : reject(err))) }) } export function eventToPromise() { let resolve const promise = new Promise(res => { resolve = res }) return { onEvent(...args) { resolve(args.length > 1 ? args : args[0]) }, fired() { return promise }, } } export async function nodeExists( client: Client, selector: string, ): Promise { const { Runtime } = client const exists = `selector => { return !!document.querySelector(selector) }` const expression = `(${exists})(\`${selector}\`)` const result = await Runtime.evaluate({ expression, }) return result.result.value } export async function getClientRect(client, selector): Promise { const { Runtime } = client const code = `selector => { const element = document.querySelector(selector) if (!element) { return undefined } const rect = element.getBoundingClientRect() return JSON.stringify({ left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, height: rect.height, width: rect.width, }) }` const expression = `(${code})(\`${selector}\`)` const result = await Runtime.evaluate({ expression }) if (!result.result.value) { throw new Error(`No element found for selector: ${selector}`) } return JSON.parse(result.result.value) as ClientRect } export async function click( client: Client, selector: string, scale: number, x?: number, y?: number, ) { const clientRect = await getClientRect(client, selector) const { Input } = client if (x === undefined) x = clientRect.width / 2 if (y === undefined) y = clientRect.height / 2 const options = { x: Math.round((clientRect.left + x) * scale), y: Math.round((clientRect.top + y) * scale), button: 'left', clickCount: 1, } await Input.dispatchMouseEvent({ ...options, type: 'mousePressed', }) await Input.dispatchMouseEvent({ ...options, type: 'mouseReleased', }) } export async function focus(client: Client, selector: string): Promise { const { DOM } = client const dom = await DOM.getDocument() const node = await DOM.querySelector({ nodeId: dom.root.nodeId, selector: selector, }) await DOM.focus(node) } export async function evaluate( client: Client, fn: string, ...args: any[] ): Promise { const { Runtime } = client const jsonArgs = JSON.stringify(args) const argStr = jsonArgs.substr(1, jsonArgs.length - 2) const expression = ` (() => { const expressionResult = (${fn})(${argStr}); if (expressionResult && expressionResult.then) { expressionResult.catch((error) => { throw new Error(error); }); return expressionResult; } return Promise.resolve(expressionResult); })(); ` const result = await Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true, }) if (result && result.exceptionDetails) { throw new Error( result.exceptionDetails.exception.value || result.exceptionDetails.exception.description, ) } if (result && result.result) { return result.result.value } return null } export async function type( client: Client, text: string, selector?: string, ): Promise { if (selector) { await focus(client, selector) await wait(500) } const { Input } = client for (let i = 0; i < text.length; i++) { const char = text[i] const options = { type: 'char', text: char, unmodifiedText: char, } await Input.dispatchKeyEvent(options) } } export async function press( client: Client, keyCode: number, count?: number, modifiers?: any, ): Promise { const { Input } = client if (count === undefined) { count = 1 } const options = { nativeVirtualKeyCode: keyCode, windowsVirtualKeyCode: keyCode, } if (modifiers) { options['modifiers'] = modifiers } for (let i = 0; i < count; i++) { await Input.dispatchKeyEvent({ ...options, type: 'rawKeyDown', }) await Input.dispatchKeyEvent({ ...options, type: 'keyUp', }) } } export async function getValue( client: Client, selector: string, ): Promise { const { Runtime } = client const browserCode = `selector => { return document.querySelector(selector).value }` const expression = `(${browserCode})(\`${selector}\`)` const result = await Runtime.evaluate({ expression, }) return result.result.value } export async function scrollTo( client: Client, x: number, y: number, ): Promise { const { Runtime } = client const browserCode = `(x, y) => { return window.scrollTo(x, y) }` const expression = `(${browserCode})(${x}, ${y})` await Runtime.evaluate({ expression, }) } export async function scrollToElement( client: Client, selector: string, ): Promise { const clientRect = await getClientRect(client, selector) return scrollTo(client, clientRect.left, clientRect.top) } export async function setHtml(client: Client, html: string): Promise { const { Page } = client const { frameTree: { frame: { id: frameId } } } = await Page.getResourceTree() await Page.setDocumentContent({ frameId, html }) } export async function getCookies( client: Client, nameOrQuery?: string | Cookie, ): Promise { const { Network } = client const fn = () => location.href const url = (await evaluate(client, `${fn}`)) as string const result = await Network.getCookies([url]) const cookies = result.cookies if (typeof nameOrQuery !== 'undefined' && typeof nameOrQuery === 'string') { const filteredCookies: Cookie[] = cookies.filter( cookie => cookie.name === nameOrQuery, ) return filteredCookies } return cookies } export async function getAllCookies(client: Client): Promise { const { Network } = client const result = await Network.getAllCookies() return result.cookies } export async function setCookies( client: Client, cookies: Cookie[], ): Promise { const { Network } = client for (const cookie of cookies) { await Network.setCookie({ ...cookie, url: cookie.url ? cookie.url : getUrlFromCookie(cookie), }) } } export async function setExtraHTTPHeaders( client: Client, headers: Headers, ): Promise { const { Network } = client await Network.setExtraHTTPHeaders({ headers }) } export async function mousedown( client: Client, selector: string, scale: number, ) { const clientRect = await getClientRect(client, selector) const { Input } = client const options = { x: Math.round((clientRect.left + clientRect.width / 2) * scale), y: Math.round((clientRect.top + clientRect.height / 2) * scale), button: 'left', clickCount: 1, } await Input.dispatchMouseEvent({ ...options, type: 'mousePressed', }) } export async function mouseup(client: Client, selector: string, scale: number) { const clientRect = await getClientRect(client, selector) const { Input } = client const options = { x: Math.round((clientRect.left + clientRect.width / 2) * scale), y: Math.round((clientRect.top + clientRect.height / 2) * scale), button: 'left', clickCount: 1, } await Input.dispatchMouseEvent({ ...options, type: 'mouseReleased', }) } function getUrlFromCookie(cookie: Cookie) { const domain = cookie.domain.slice(1, cookie.domain.length) return `https://${domain}` } export async function deleteCookie( client: Client, name: string, url: string, ): Promise { const { Network } = client await Network.deleteCookie({ cookieName: name, url }) } export async function clearCookies(client: Client): Promise { const { Network } = client await Network.clearBrowserCookies() } export async function getBoxModel( client: Client, selector: string, ): Promise { const { DOM } = client const { root: { nodeId: documentNodeId } } = await DOM.getDocument() const { nodeId } = await DOM.querySelector({ selector: selector, nodeId: documentNodeId, }) const { model } = await DOM.getBoxModel({ nodeId }) return model } export function boxModelToViewPort(model: BoxModel, scale: number): Viewport { return { x: model.content[0], y: model.content[1], width: model.width, height: model.height, scale, } } export async function screenshot( client: Client, selector: string, options: ScreenshotOptions, ): Promise { const { Page } = client const captureScreenshotOptions = { format: 'png', fromSurface: true, clip: undefined, } if (selector) { const model = await getBoxModel(client, selector) captureScreenshotOptions.clip = boxModelToViewPort(model, 1) } if (options && options.omitBackground) client.Emulation.setDefaultBackgroundColorOverride({ color: { r: 0, g: 0, b: 0, a: 0 }, }) const screenshot = await Page.captureScreenshot(captureScreenshotOptions) if (options && options.omitBackground) client.Emulation.setDefaultBackgroundColorOverride() return screenshot.data } export async function html(client: Client): Promise { const { DOM } = client const { root: { nodeId } } = await DOM.getDocument() const { outerHTML } = await DOM.getOuterHTML({ nodeId }) return outerHTML } export async function htmlUrl(client: Client): Promise { const { DOM } = client const { root: { nodeId } } = await DOM.getDocument() const { outerHTML } = await DOM.getOuterHTML({ nodeId }) return outerHTML } export async function pdf( client: Client, options?: PdfOptions, ): Promise { const { Page } = client const pdf = await Page.printToPDF(options) return pdf.data } export async function clearInput( client: Client, selector: string, ): Promise { await wait(500) await focus(client, selector) const { Input } = client const text = await getValue(client, selector) const optionsDelete = { nativeVirtualKeyCode: 46, windowsVirtualKeyCode: 46, } const optionsBackspace = { nativeVirtualKeyCode: 8, windowsVirtualKeyCode: 8, } for (let i = 0; i < text.length; i++) { await Input.dispatchKeyEvent({ ...optionsDelete, type: 'rawKeyDown', }) Input.dispatchKeyEvent({ ...optionsDelete, type: 'keyUp', }) await Input.dispatchKeyEvent({ ...optionsBackspace, type: 'rawKeyDown', }) Input.dispatchKeyEvent({ ...optionsBackspace, type: 'keyUp', }) } } export async function setFileInput( client: Client, selector: string, files: string[], ): Promise { const { DOM } = client const dom = await DOM.getDocument() const node = await DOM.querySelector({ nodeId: dom.root.nodeId, selector: selector, }) return await DOM.setFileInputFiles({ files: files, nodeId: node.nodeId }) } export function getDebugOption(): boolean { if ( process && process.env && process.env['DEBUG'] && process.env['DEBUG'].includes('chromeless') ) { return true } return false } export function writeToFile( data: string, extension: string, filePathOverride: string, ): string { const filePath = filePathOverride || path.join(os.tmpdir(), `${cuid()}.${extension}`) fs.writeFileSync(filePath, Buffer.from(data, 'base64')) return filePath } function getS3BucketName() { return process.env['CHROMELESS_S3_BUCKET_NAME'] } function getS3BucketUrl() { return process.env['CHROMELESS_S3_BUCKET_URL'] } function getS3ObjectKeyPrefix() { return process.env['CHROMELESS_S3_OBJECT_KEY_PREFIX'] || '' } function getS3FilesPermissions() { return process.env['CHROMELESS_S3_OBJECT_ACL'] || 'public-read' } export function isS3Configured() { return getS3BucketName() && getS3BucketUrl() } const s3ContentTypes = { 'image/png': { extension: 'png', }, 'application/pdf': { extension: 'pdf', }, 'text/html': { extension: 'html', }, } export async function uploadToS3( data: string, contentType: string, ): Promise { const s3ContentType = s3ContentTypes[contentType] if (!s3ContentType) { throw new Error(`Unknown S3 Content type ${contentType}`) } const s3Path = `${getS3ObjectKeyPrefix()}${cuid()}.${s3ContentType.extension}` const s3 = new AWS.S3() await s3 .putObject({ Bucket: getS3BucketName(), Key: s3Path, ContentType: contentType, ACL: getS3FilesPermissions(), Body: Buffer.from(data, contentType === 'text/html' ? 'utf8' : 'base64'), }) .promise() return `https://${getS3BucketUrl()}/${s3Path}` } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es5", "moduleResolution": "node", "sourceMap": true, "outDir": "dist", "lib": ["es6", "dom"], "strictNullChecks": false, "pretty": true, "rootDir": ".", "forceConsistentCasingInFileNames": true }, "exclude": ["node_modules", "node_modules/**", "dist"], "include": ["./src/**/*.ts", "./examples/**/*.ts"] } ================================================ FILE: tslint.json ================================================ { "rules": { "class-name": true, "comment-format": [true, "check-space"], "no-var-keyword": true, "no-internal-module": true, "no-null-keyword": false, "prefer-const": true, "jsdoc-format": true } }