first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = require('./lib/DeviceDescriptors');
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = require('./lib/Errors');
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+437
View File
@@ -0,0 +1,437 @@
# Puppeteer
<!-- [START badges] -->
[![Build status](https://img.shields.io/travis/com/puppeteer/puppeteer/master.svg)](https://travis-ci.com/puppeteer/puppeteer) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) [![Issue resolution status](https://isitmaintained.com/badge/resolution/puppeteer/puppeteer.svg)](https://github.com/puppeteer/puppeteer/issues)
<!-- [END badges] -->
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right">
###### [API](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md)
> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium.
<!-- [START usecases] -->
###### What can I do?
Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started:
* Generate screenshots and PDFs of pages.
* Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)).
* Automate form submission, UI testing, keyboard input, etc.
* Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features.
* Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues.
* Test Chrome Extensions.
<!-- [END usecases] -->
Give it a spin: https://try-puppeteer.appspot.com/
<!-- [START getstarted] -->
## Getting Started
### Installation
To use Puppeteer in your project, run:
```bash
npm i puppeteer
# or "yarn add puppeteer"
```
Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#environment-variables).
### puppeteer-core
Since version 1.7.0 we publish the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package,
a version of Puppeteer that doesn't download any browser by default.
```bash
npm i puppeteer-core
# or "yarn add puppeteer-core"
```
`puppeteer-core` is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one. Be sure that the version of puppeteer-core you install is compatible with the
browser you intend to connect to.
See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#puppeteer-vs-puppeteer-core).
### Usage
Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node.
Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on
Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater.
Puppeteer will be familiar to people using other browser testing frameworks. You create an instance
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#).
**Example** - navigating to https://example.com and saving a screenshot as *example.png*:
Save file as **example.js**
```js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();
```
Execute script on the command line
```bash
node example.js
```
Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#pagesetviewportviewport).
**Example** - create a PDF.
Save file as **hn.js**
```js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
await page.pdf({path: 'hn.pdf', format: 'A4'});
await browser.close();
})();
```
Execute script on the command line
```bash
node hn.js
```
See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#pagepdfoptions) for more information about creating pdfs.
**Example** - evaluate script in the context of the page
Save file as **get-dimensions.js**
```js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Get the "viewport" of the page, as reported by the page.
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
};
});
console.log('Dimensions:', dimensions);
await browser.close();
})();
```
Execute script on the command line
```bash
node get-dimensions.js
```
See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`.
<!-- [END getstarted] -->
<!-- [START runtimesettings] -->
## Default runtime settings
**1. Uses Headless mode**
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#puppeteerlaunchoptions) when launching a browser:
```js
const browser = await puppeteer.launch({headless: false}); // default is true
```
**2. Runs a bundled version of Chromium**
By default, Puppeteer downloads and uses a specific version of Chromium so its API
is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium,
pass in the executable's path when creating a `Browser` instance:
```js
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
```
You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#puppeteerlaunchoptions) for more information.
See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/master/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users.
**3. Creates a fresh user profile**
Puppeteer creates its own browser user profile which it **cleans up on every run**.
<!-- [END runtimesettings] -->
## Resources
- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md)
- [Examples](https://github.com/puppeteer/puppeteer/tree/master/examples/)
- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer)
<!-- [START debugging] -->
## Debugging tips
1. Turn off headless mode - sometimes it's useful to see what the browser is
displaying. Instead of launching in headless mode, launch a full version of
the browser using `headless: false`:
```js
const browser = await puppeteer.launch({headless: false});
```
2. Slow it down - the `slowMo` option slows down Puppeteer operations by the
specified amount of milliseconds. It's another way to help see what's going on.
```js
const browser = await puppeteer.launch({
headless: false,
slowMo: 250 // slow down by 250ms
});
```
3. Capture console output - You can listen for the `console` event.
This is also handy when debugging code in `page.evaluate()`:
```js
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.evaluate(() => console.log(`url is ${location.href}`));
```
4. Use debugger in application code browser
There are two execution context: node.js that is running test code, and the browser
running application code being tested. This lets you debug code in the
application code browser; ie code inside `evaluate()`.
- Use `{devtools: true}` when launching Puppeteer:
```js
const browser = await puppeteer.launch({devtools: true});
```
- Change default test timeout:
jest: `jest.setTimeout(100000);`
jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;`
mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442))
- Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement:
```js
await page.evaluate(() => {debugger;});
```
The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode.
5. Use debugger in node.js
This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser.
Note that you won't be able to run `await page.click()` in
DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if
you want to try something out, you have to add it to your test file.
- Add `debugger;` to your test, eg:
```js
debugger;
await page.click('a[target=_blank]');
```
- Set `headless` to `false`
- Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests`
- In Chrome open `chrome://inspect/#devices` and click `inspect`
- In the newly opened test browser, type `F8` to resume test execution
- Now your `debugger` will be hit and you can debug in the test browser
6. Enable verbose logging - internal DevTools protocol traffic
will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace.
# Basic verbose logging
env DEBUG="puppeteer:*" node script.js
# Protocol traffic can be rather noisy. This example filters out all Network domain messages
env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'
7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb)
- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!)
- add a `debugger` to your Puppeteer (node) code
- add `ndb` (or `npx ndb`) before your test command. For example:
`ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`)
- debug your test inside chromium like a boss!
<!-- [END debugging] -->
## Contributing to Puppeteer
Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md) to get an overview of Puppeteer development.
<!-- [START faq] -->
# FAQ
#### Q: Who maintains Puppeteer?
The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project!
See [Contributing](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md).
#### Q: What is the status of cross-browser support?
Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention.
From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox.
We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari.
This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome).
#### Q: What are Puppeteers goals and principles?
The goals of the project are:
- Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer.
- Grow the adoption of headless/automated browser testing.
- Help dogfood new DevTools Protocol features...and catch bugs!
- Learn more about the pain points of automated browser testing and help fill those gaps.
We adapt [Chromium principles](https://www.chromium.org/developers/core-principles) to help us drive product decisions:
- **Speed**: Puppeteer has almost zero performance overhead over an automated page.
- **Security**: Puppeteer operates off-process with respect to Chromium, making it safe to automate potentially malicious pages.
- **Stability**: Puppeteer should not be flaky and should not leak memory.
- **Simplicity**: Puppeteer provides a high-level API thats easy to use, understand, and debug.
#### Q: Is Puppeteer replacing Selenium/WebDriver?
**No**. Both projects are valuable for very different reasons:
- Selenium/WebDriver focuses on cross-browser automation; its value proposition is a single standard API that works across all major browsers.
- Puppeteer focuses on Chromium; its value proposition is richer functionality and higher reliability.
That said, you **can** use Puppeteer to run tests against Chromium, e.g. using the community-driven [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer). While this probably shouldnt be your only testing solution, it does have a few good points compared to WebDriver:
- Puppeteer requires zero setup and comes bundled with the Chromium version it works best with, making it [very easy to start with](https://github.com/puppeteer/puppeteer/#getting-started). At the end of the day, its better to have a few tests running chromium-only, than no tests at all.
- Puppeteer has event-driven architecture, which removes a lot of potential flakiness. Theres no need for evil “sleep(1000)” calls in puppeteer scripts.
- Puppeteer runs headless by default, which makes it fast to run. Puppeteer v1.5.0 also exposes browser contexts, making it possible to efficiently parallelize test execution.
- Puppeteer shines when it comes to debugging: flip the “headless” bit to false, add “slowMo”, and youll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment.
#### Q: Why doesnt Puppeteer v.XXX work with Chromium v.YYY?
We see Puppeteer as an **indivisible entity** with Chromium. Each version of Puppeteer bundles a specific version of Chromium **the only** version it is guaranteed to work with.
This is not an artificial constraint: A lot of work on Puppeteer is actually taking place in the Chromium repository. Heres a typical story:
- A Puppeteer bug is reported: https://github.com/puppeteer/puppeteer/issues/2709
- It turned out this is an issue with the DevTools protocol, so were fixing it in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/1102154
- Once the upstream fix is landed, we roll updated Chromium into Puppeteer: https://github.com/puppeteer/puppeteer/pull/2769
However, oftentimes it is desirable to use Puppeteer with the official Google Chrome rather than Chromium. For this to work, you should install a `puppeteer-core` version that corresponds to the Chrome version.
For example, in order to drive Chrome 71 with puppeteer-core, use `chrome-71` npm tag:
```bash
npm install puppeteer-core@chrome-71
```
#### Q: Which Chromium version does Puppeteer use?
Look for `chromium_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/master/package.json). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section.
#### Q: Which Firefox version does Puppeteer use?
Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/master/package.json) is `latest` -- Puppeteer isn't tied to a particular Firefox version.
To fetch Firefox Nightly as part of Puppeteer installation:
```bash
PUPPETEER_PRODUCT=firefox npm i puppeteer
# or "yarn add puppeteer"
```
#### Q: Whats considered a “Navigation”?
From Puppeteers standpoint, **“navigation” is anything that changes a pages URL**.
Aside from regular navigation where the browser hits the network to fetch a new document from the web server, this includes [anchor navigations](https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid) and [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) usage.
With this definition of “navigation,” **Puppeteer works seamlessly with single-page applications.**
#### Q: Whats the difference between a “trusted" and "untrusted" input event?
In browsers, input events could be divided into two big groups: trusted vs. untrusted.
- **Trusted events**: events generated by users interacting with the page, e.g. using a mouse or keyboard.
- **Untrusted event**: events generated by Web APIs, e.g. `document.createEvent` or `element.click()` methods.
Websites can distinguish between these two groups:
- using an [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) event flag
- sniffing for accompanying events. For example, every trusted `'click'` event is preceded by `'mousedown'` and `'mouseup'` events.
For automation purposes its important to generate trusted events. **All input events generated with Puppeteer are trusted and fire proper accompanying events.** If, for some reason, one needs an untrusted event, its always possible to hop into a page context with `page.evaluate` and generate a fake event:
```js
await page.evaluate(() => {
document.querySelector('button[type=submit]').click();
});
```
#### Q: What features does Puppeteer not support?
You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this:
* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.)
* Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming).
#### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help?
We have a [troubleshooting](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md) guide for various operating systems that lists the required dependencies.
#### Q: How do I try/test a prerelease version of Puppeteer?
You can check out this repo or install the latest prerelease from npm:
```bash
npm i --save puppeteer@next
```
Please note that prerelease may be unstable and contain bugs.
#### Q: I have more questions! Where do I ask?
There are many ways to get help on Puppeteer:
- [bugtracker](https://github.com/puppeteer/puppeteer/issues)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/puppeteer)
- [slack channel](https://join.slack.com/t/puppeteer/shared_invite/enQtMzU4MjIyMDA5NTM4LWI0YTE0MjM0NWQzYmE2MTRmNjM1ZTBkN2MxNmJmNTIwNTJjMmFhOWFjMGExMDViYjk2YjU2ZmYzMmE1NmExYzc)
Make sure to search these channels before posting your question.
<!-- [END faq] -->
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { helper } = require('./lib/helper');
const api = require('./lib/api');
const { Page } = require('./lib/Page');
for (const className in api) {
if (typeof api[className] === 'function')
helper.installAsyncStackHooks(api[className]);
}
// Expose alias for deprecated method.
Page.prototype.emulateMedia = Page.prototype.emulateMediaType;
const { Puppeteer } = require('./lib/Puppeteer');
const packageJson = require('./package.json');
let preferredRevision = packageJson.puppeteer.chromium_revision;
const isPuppeteerCore = packageJson.name === 'puppeteer-core';
// puppeteer-core ignores environment variables
const product = isPuppeteerCore
? undefined
: process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
if (!isPuppeteerCore && product === 'firefox')
preferredRevision = packageJson.puppeteer.firefox_revision;
const puppeteer = new Puppeteer(
__dirname,
preferredRevision,
isPuppeteerCore,
product
);
// The introspection in `Helper.installAsyncStackHooks` references `Puppeteer._launcher`
// before the Puppeteer ctor is called, such that an invalid Launcher is selected at import,
// so we reset it.
puppeteer._lazyLauncher = undefined;
module.exports = puppeteer;
+242
View File
@@ -0,0 +1,242 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This file is part of public API.
*
* By default, the `puppeteer` package runs this script during the installation
* process unless one of the env flags is provided.
* `puppeteer-core` package doesn't include this step at all. However, it's
* still possible to install a supported browser using this script when
* necessary.
*/
const compileTypeScriptIfRequired = require('./typescript-if-required');
const firefoxVersions =
'https://product-details.mozilla.org/1.0/firefox_versions.json';
const supportedProducts = {
chrome: 'Chromium',
firefox: 'Firefox Nightly',
};
async function download() {
await compileTypeScriptIfRequired();
const downloadHost =
process.env.PUPPETEER_DOWNLOAD_HOST ||
process.env.npm_config_puppeteer_download_host ||
process.env.npm_package_config_puppeteer_download_host;
const puppeteer = require('./index');
const product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product ||
'chrome';
const browserFetcher = puppeteer.createBrowserFetcher({
product,
host: downloadHost,
});
const revision = await getRevision();
await fetchBinary(revision);
function getRevision() {
if (product === 'chrome') {
return (
process.env.PUPPETEER_CHROMIUM_REVISION ||
process.env.npm_config_puppeteer_chromium_revision ||
process.env.npm_package_config_puppeteer_chromium_revision ||
require('./package.json').puppeteer.chromium_revision
);
} else if (product === 'firefox') {
puppeteer._preferredRevision = require('./package.json').puppeteer.firefox_revision;
return getFirefoxNightlyVersion(browserFetcher.host()).catch((error) => {
console.error(error);
process.exit(1);
});
} else {
throw new Error(`Unsupported product ${product}`);
}
}
function fetchBinary(revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
// Do nothing if the revision is already downloaded.
if (revisionInfo.local) {
logPolitely(
`${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.`
);
return;
}
// Override current environment proxy settings with npm configuration, if any.
const NPM_HTTPS_PROXY =
process.env.npm_config_https_proxy || process.env.npm_config_proxy;
const NPM_HTTP_PROXY =
process.env.npm_config_http_proxy || process.env.npm_config_proxy;
const NPM_NO_PROXY = process.env.npm_config_no_proxy;
if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY;
if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY;
/**
* @param {!Array<string>}
* @return {!Promise}
*/
function onSuccess(localRevisions) {
logPolitely(
`${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}`
);
localRevisions = localRevisions.filter(
(revision) => revision !== revisionInfo.revision
);
const cleanupOldVersions = localRevisions.map((revision) =>
browserFetcher.remove(revision)
);
Promise.all([...cleanupOldVersions]);
}
/**
* @param {!Error} error
*/
function onError(error) {
console.error(
`ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`
);
console.error(error);
process.exit(1);
}
let progressBar = null;
let lastDownloadedBytes = 0;
function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(
`Downloading ${
supportedProducts[product]
} r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}
return browserFetcher
.download(revisionInfo.revision, onProgress)
.then(() => browserFetcher.localRevisions())
.then(onSuccess)
.catch(onError);
}
function toMegabytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}
function getFirefoxNightlyVersion(host) {
const https = require('https');
const promise = new Promise((resolve, reject) => {
let data = '';
logPolitely(`Requesting latest Firefox Nightly version from ${host}`);
https
.get(firefoxVersions, (r) => {
if (r.statusCode >= 400)
return reject(new Error(`Got status code ${r.statusCode}`));
r.on('data', (chunk) => {
data += chunk;
});
r.on('end', () => {
try {
const versions = JSON.parse(data);
return resolve(versions.FIREFOX_NIGHTLY);
} catch {
return reject(new Error('Firefox version not found'));
}
});
})
.on('error', reject);
});
return promise;
}
}
function logPolitely(toBeLogged) {
const logLevel = process.env.npm_config_loglevel;
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
if (!logLevelDisplay) console.log(toBeLogged);
}
if (process.env.PUPPETEER_SKIP_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.'
);
return;
}
if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.'
);
return;
}
download();
+295
View File
@@ -0,0 +1,295 @@
"use strict";
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Accessibility = void 0;
class Accessibility {
constructor(client) {
this._client = client;
}
async snapshot(options = {}) {
const { interestingOnly = true, root = null } = options;
const { nodes } = await this._client.send('Accessibility.getFullAXTree');
let backendNodeId = null;
if (root) {
const { node } = await this._client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId,
});
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(nodes);
let needle = defaultRoot;
if (backendNodeId) {
needle = defaultRoot.find((node) => node.payload.backendDOMNodeId === backendNodeId);
if (!needle)
return null;
}
if (!interestingOnly)
return this.serializeTree(needle)[0];
const interestingNodes = new Set();
this.collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle))
return null;
return this.serializeTree(needle, interestingNodes)[0];
}
serializeTree(node, whitelistedNodes) {
const children = [];
for (const child of node.children)
children.push(...this.serializeTree(child, whitelistedNodes));
if (whitelistedNodes && !whitelistedNodes.has(node))
return children;
const serializedNode = node.serialize();
if (children.length)
serializedNode.children = children;
return [serializedNode];
}
collectInterestingNodes(collection, node, insideControl) {
if (node.isInteresting(insideControl))
collection.add(node);
if (node.isLeafNode())
return;
insideControl = insideControl || node.isControl();
for (const child of node.children)
this.collectInterestingNodes(collection, child, insideControl);
}
}
exports.Accessibility = Accessibility;
class AXNode {
constructor(payload) {
this.children = [];
this._richlyEditable = false;
this._editable = false;
this._focusable = false;
this._hidden = false;
this.payload = payload;
this._name = this.payload.name ? this.payload.name.value : '';
this._role = this.payload.role ? this.payload.role.value : 'Unknown';
for (const property of this.payload.properties || []) {
if (property.name === 'editable') {
this._richlyEditable = property.value.value === 'richtext';
this._editable = true;
}
if (property.name === 'focusable')
this._focusable = property.value.value;
if (property.name === 'hidden')
this._hidden = property.value.value;
}
}
_isPlainTextField() {
if (this._richlyEditable)
return false;
if (this._editable)
return true;
return (this._role === 'textbox' ||
this._role === 'ComboBox' ||
this._role === 'searchbox');
}
_isTextOnlyObject() {
const role = this._role;
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
}
_hasFocusableChild() {
if (this._cachedHasFocusableChild === undefined) {
this._cachedHasFocusableChild = false;
for (const child of this.children) {
if (child._focusable || child._hasFocusableChild()) {
this._cachedHasFocusableChild = true;
break;
}
}
}
return this._cachedHasFocusableChild;
}
find(predicate) {
if (predicate(this))
return this;
for (const child of this.children) {
const result = child.find(predicate);
if (result)
return result;
}
return null;
}
isLeafNode() {
if (!this.children.length)
return true;
// These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (this._isPlainTextField() || this._isTextOnlyObject())
return true;
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
// (Note that whilst ARIA buttons can have only presentational children, HTML5
// buttons are allowed to have content.)
switch (this._role) {
case 'doc-cover':
case 'graphics-symbol':
case 'img':
case 'Meter':
case 'scrollbar':
case 'slider':
case 'separator':
case 'progressbar':
return true;
default:
break;
}
// Here and below: Android heuristics
if (this._hasFocusableChild())
return false;
if (this._focusable && this._name)
return true;
if (this._role === 'heading' && this._name)
return true;
return false;
}
isControl() {
switch (this._role) {
case 'button':
case 'checkbox':
case 'ColorWell':
case 'combobox':
case 'DisclosureTriangle':
case 'listbox':
case 'menu':
case 'menubar':
case 'menuitem':
case 'menuitemcheckbox':
case 'menuitemradio':
case 'radio':
case 'scrollbar':
case 'searchbox':
case 'slider':
case 'spinbutton':
case 'switch':
case 'tab':
case 'textbox':
case 'tree':
return true;
default:
return false;
}
}
isInteresting(insideControl) {
const role = this._role;
if (role === 'Ignored' || this._hidden)
return false;
if (this._focusable || this._richlyEditable)
return true;
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl())
return true;
// A non focusable child of a control is not interesting
if (insideControl)
return false;
return this.isLeafNode() && !!this._name;
}
serialize() {
const properties = new Map();
for (const property of this.payload.properties || [])
properties.set(property.name.toLowerCase(), property.value.value);
if (this.payload.name)
properties.set('name', this.payload.name.value);
if (this.payload.value)
properties.set('value', this.payload.value.value);
if (this.payload.description)
properties.set('description', this.payload.description.value);
const node = {
role: this._role,
};
const userStringProperties = [
'name',
'value',
'description',
'keyshortcuts',
'roledescription',
'valuetext',
];
const getUserStringPropertyValue = (key) => properties.get(key);
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty))
continue;
node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
}
const booleanProperties = [
'disabled',
'expanded',
'focused',
'modal',
'multiline',
'multiselectable',
'readonly',
'required',
'selected',
];
const getBooleanPropertyValue = (key) => properties.get(key);
for (const booleanProperty of booleanProperties) {
// WebArea's treat focus differently than other nodes. They report whether their frame has focus,
// not whether focus is specifically on the root node.
if (booleanProperty === 'focused' && this._role === 'WebArea')
continue;
const value = getBooleanPropertyValue(booleanProperty);
if (!value)
continue;
node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
}
const tristateProperties = ['checked', 'pressed'];
for (const tristateProperty of tristateProperties) {
if (!properties.has(tristateProperty))
continue;
const value = properties.get(tristateProperty);
node[tristateProperty] =
value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
}
const numericalProperties = [
'level',
'valuemax',
'valuemin',
];
const getNumericalPropertyValue = (key) => properties.get(key);
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty))
continue;
node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
}
const tokenProperties = [
'autocomplete',
'haspopup',
'invalid',
'orientation',
];
const getTokenPropertyValue = (key) => properties.get(key);
for (const tokenProperty of tokenProperties) {
const value = getTokenPropertyValue(tokenProperty);
if (!value || value === 'false')
continue;
node[tokenProperty] = getTokenPropertyValue(tokenProperty);
}
return node;
}
static createTree(payloads) {
const nodeById = new Map();
for (const payload of payloads)
nodeById.set(payload.nodeId, new AXNode(payload));
for (const node of nodeById.values()) {
for (const childId of node.payload.childIds || [])
node.children.push(nodeById.get(childId));
}
return nodeById.values().next().value;
}
}
+264
View File
@@ -0,0 +1,264 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserContext = exports.Browser = void 0;
const helper_1 = require("./helper");
const Target_1 = require("./Target");
const EventEmitter = require("events");
const Events_1 = require("./Events");
class Browser extends EventEmitter {
constructor(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
super();
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
this._process = process;
this._connection = connection;
this._closeCallback = closeCallback || function () { };
this._defaultContext = new BrowserContext(this._connection, this, null);
this._contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));
this._targets = new Map();
this._connection.on(Events_1.Events.Connection.Disconnected, () => this.emit(Events_1.Events.Browser.Disconnected));
this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
}
static async create(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback);
await connection.send('Target.setDiscoverTargets', { discover: true });
return browser;
}
process() {
return this._process;
}
async createIncognitoBrowserContext() {
const { browserContextId } = await this._connection.send('Target.createBrowserContext');
const context = new BrowserContext(this._connection, this, browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts() {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext() {
return this._defaultContext;
}
/**
* @param {?string} contextId
*/
async _disposeContext(contextId) {
await this._connection.send('Target.disposeBrowserContext', {
browserContextId: contextId || undefined,
});
this._contexts.delete(contextId);
}
async _targetCreated(event) {
const targetInfo = event.targetInfo;
const { browserContextId } = targetInfo;
const context = browserContextId && this._contexts.has(browserContextId)
? this._contexts.get(browserContextId)
: this._defaultContext;
const target = new Target_1.Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport);
helper_1.assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(event.targetInfo.targetId, target);
if (await target._initializedPromise) {
this.emit(Events_1.Events.Browser.TargetCreated, target);
context.emit(Events_1.Events.BrowserContext.TargetCreated, target);
}
}
/**
* @param {{targetId: string}} event
*/
async _targetDestroyed(event) {
const target = this._targets.get(event.targetId);
target._initializedCallback(false);
this._targets.delete(event.targetId);
target._closedCallback();
if (await target._initializedPromise) {
this.emit(Events_1.Events.Browser.TargetDestroyed, target);
target
.browserContext()
.emit(Events_1.Events.BrowserContext.TargetDestroyed, target);
}
}
/**
* @param {!Protocol.Target.targetInfoChangedPayload} event
*/
_targetInfoChanged(event) {
const target = this._targets.get(event.targetInfo.targetId);
helper_1.assert(target, 'target should exist before targetInfoChanged');
const previousURL = target.url();
const wasInitialized = target._isInitialized;
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) {
this.emit(Events_1.Events.Browser.TargetChanged, target);
target.browserContext().emit(Events_1.Events.BrowserContext.TargetChanged, target);
}
}
wsEndpoint() {
return this._connection.url();
}
async newPage() {
return this._defaultContext.newPage();
}
async _createPageInContext(contextId) {
const { targetId } = await this._connection.send('Target.createTarget', {
url: 'about:blank',
browserContextId: contextId || undefined,
});
const target = await this._targets.get(targetId);
helper_1.assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page;
}
targets() {
return Array.from(this._targets.values()).filter((target) => target._isInitialized);
}
target() {
return this.targets().find((target) => target.type() === 'browser');
}
/**
* @param {function(!Target):boolean} predicate
* @param {{timeout?: number}=} options
* @return {!Promise<!Target>}
*/
async waitForTarget(predicate, options = {}) {
const { timeout = 30000 } = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve;
const targetPromise = new Promise((x) => (resolve = x));
this.on(Events_1.Events.Browser.TargetCreated, check);
this.on(Events_1.Events.Browser.TargetChanged, check);
try {
if (!timeout)
return await targetPromise;
return await helper_1.helper.waitWithTimeout(targetPromise, 'target', timeout);
}
finally {
this.removeListener(Events_1.Events.Browser.TargetCreated, check);
this.removeListener(Events_1.Events.Browser.TargetChanged, check);
}
function check(target) {
if (predicate(target))
resolve(target);
}
}
async pages() {
const contextPages = await Promise.all(this.browserContexts().map((context) => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
}
async version() {
const version = await this._getVersion();
return version.product;
}
async userAgent() {
const version = await this._getVersion();
return version.userAgent;
}
async close() {
await this._closeCallback.call(null);
this.disconnect();
}
disconnect() {
this._connection.dispose();
}
isConnected() {
return !this._connection._closed;
}
_getVersion() {
return this._connection.send('Browser.getVersion');
}
}
exports.Browser = Browser;
class BrowserContext extends EventEmitter {
constructor(connection, browser, contextId) {
super();
this._connection = connection;
this._browser = browser;
this._id = contextId;
}
targets() {
return this._browser
.targets()
.filter((target) => target.browserContext() === this);
}
waitForTarget(predicate, options) {
return this._browser.waitForTarget((target) => target.browserContext() === this && predicate(target), options);
}
async pages() {
const pages = await Promise.all(this.targets()
.filter((target) => target.type() === 'page')
.map((target) => target.page()));
return pages.filter((page) => !!page);
}
isIncognito() {
return !!this._id;
}
async overridePermissions(origin, permissions) {
const webPermissionToProtocol = new Map([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
permissions = permissions.map((permission) => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {
origin,
browserContextId: this._id || undefined,
permissions,
});
}
async clearPermissionOverrides() {
await this._connection.send('Browser.resetPermissions', {
browserContextId: this._id || undefined,
});
}
newPage() {
return this._browser._createPageInContext(this._id);
}
browser() {
return this._browser;
}
async close() {
helper_1.assert(this._id, 'Non-incognito profiles cannot be closed!');
await this._browser._disposeContext(this._id);
}
}
exports.BrowserContext = BrowserContext;
+406
View File
@@ -0,0 +1,406 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserFetcher = void 0;
const os = require("os");
const fs = require("fs");
const path = require("path");
const util = require("util");
const childProcess = require("child_process");
const https = require("https");
const http = require("http");
const extractZip = require("extract-zip");
const debug = require("debug");
const removeRecursive = require("rimraf");
const URL = require("url");
const ProxyAgent = require("https-proxy-agent");
const proxy_from_env_1 = require("proxy-from-env");
const helper_1 = require("./helper");
const debugFetcher = debug(`puppeteer:fetcher`);
const downloadURLs = {
chrome: {
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip',
win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip',
},
firefox: {
linux: '%s/firefox-%s.en-US.%s-x86_64.tar.bz2',
mac: '%s/firefox-%s.en-US.%s.dmg',
win32: '%s/firefox-%s.en-US.%s.zip',
win64: '%s/firefox-%s.en-US.%s.zip',
},
};
const browserConfig = {
chrome: {
host: 'https://storage.googleapis.com',
destination: '.local-chromium',
},
firefox: {
host: 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central',
destination: '.local-firefox',
},
};
function archiveName(product, platform, revision) {
if (product === 'chrome') {
if (platform === 'linux')
return 'chrome-linux';
if (platform === 'mac')
return 'chrome-mac';
if (platform === 'win32' || platform === 'win64') {
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
}
}
else if (product === 'firefox') {
return platform;
}
}
/**
* @param {string} product
* @param {string} platform
* @param {string} host
* @param {string} revision
* @return {string}
*/
function downloadURL(product, platform, host, revision) {
const url = util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
return url;
}
const readdirAsync = helper_1.helper.promisify(fs.readdir.bind(fs));
const mkdirAsync = helper_1.helper.promisify(fs.mkdir.bind(fs));
const unlinkAsync = helper_1.helper.promisify(fs.unlink.bind(fs));
const chmodAsync = helper_1.helper.promisify(fs.chmod.bind(fs));
function existsAsync(filePath) {
return new Promise((resolve) => {
fs.access(filePath, (err) => resolve(!err));
});
}
/**
*/
class BrowserFetcher {
constructor(projectRoot, options = {}) {
this._product = (options.product || 'chrome').toLowerCase();
helper_1.assert(this._product === 'chrome' || this._product === 'firefox', `Unknown product: "${options.product}"`);
this._downloadsFolder =
options.path ||
path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host;
this.setPlatform(options.platform);
helper_1.assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
}
setPlatform(platformFromOptions) {
if (platformFromOptions) {
this._platform = platformFromOptions;
return;
}
const platform = os.platform();
if (platform === 'darwin')
this._platform = 'mac';
else if (platform === 'linux')
this._platform = 'linux';
else if (platform === 'win32')
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
else
helper_1.assert(this._platform, 'Unsupported platform: ' + os.platform());
}
platform() {
return this._platform;
}
product() {
return this._product;
}
host() {
return this._downloadHost;
}
canDownload(revision) {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
return new Promise((resolve) => {
const request = httpRequest(url, 'HEAD', (response) => {
resolve(response.statusCode === 200);
});
request.on('error', (error) => {
console.error(error);
resolve(false);
});
});
}
/**
* @param {string} revision
* @param {?function(number, number):void} progressCallback
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
*/
async download(revision, progressCallback) {
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
const fileName = url.split('/').pop();
const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision);
if (await existsAsync(outputPath))
return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder)))
await mkdirAsync(this._downloadsFolder);
try {
await downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath);
}
finally {
if (await existsAsync(archivePath))
await unlinkAsync(archivePath);
}
const revisionInfo = this.revisionInfo(revision);
if (revisionInfo)
await chmodAsync(revisionInfo.executablePath, 0o755);
return revisionInfo;
}
async localRevisions() {
if (!(await existsAsync(this._downloadsFolder)))
return [];
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(this._product, fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}
async remove(revision) {
const folderPath = this._getFolderPath(revision);
helper_1.assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
await new Promise((fulfill) => removeRecursive(folderPath, fulfill));
}
revisionInfo(revision) {
const folderPath = this._getFolderPath(revision);
let executablePath = '';
if (this._product === 'chrome') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
else if (this._platform === 'linux')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
}
else if (this._product === 'firefox') {
if (this._platform === 'mac')
executablePath = path.join(folderPath, 'Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
else if (this._platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else
throw new Error('Unsupported platform: ' + this._platform);
}
else {
throw new Error('Unsupported product: ' + this._product);
}
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
const local = fs.existsSync(folderPath);
debugFetcher({
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
});
return {
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
};
}
/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision) {
return path.join(this._downloadsFolder, this._platform + '-' + revision);
}
}
exports.BrowserFetcher = BrowserFetcher;
function parseFolderPath(product, folderPath) {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2)
return null;
const [platform, revision] = splits;
if (!downloadURLs[product][platform])
return null;
return { product, platform, revision };
}
/**
* @param {string} url
* @param {string} destinationPath
* @param {?function(number, number):void} progressCallback
* @return {!Promise}
*/
function downloadFile(url, destinationPath, progressCallback) {
debugFetcher(`Downloading binary from ${url}`);
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
const promise = new Promise((x, y) => {
fulfill = x;
reject = y;
});
const request = httpRequest(url, 'GET', (response) => {
if (response.statusCode !== 200) {
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
// consume response data to free up memory
response.resume();
reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', (error) => reject(error));
response.pipe(file);
totalBytes = parseInt(
/** @type {string} */ response.headers['content-length'], 10);
if (progressCallback)
response.on('data', onData);
});
request.on('error', (error) => reject(error));
return promise;
function onData(chunk) {
downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
}
}
function install(archivePath, folderPath) {
debugFetcher(`Installing ${archivePath} to ${folderPath}`);
if (archivePath.endsWith('.zip'))
return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2'))
return extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg'))
return mkdirAsync(folderPath).then(() => installDMG(archivePath, folderPath));
else
throw new Error(`Unsupported archive format: ${archivePath}`);
}
/**
* @param {string} tarPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function extractTar(tarPath, folderPath) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tar = require('tar-fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const bzip = require('unbzip2-stream');
return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath);
tarStream.on('error', reject);
tarStream.on('finish', fulfill);
const readStream = fs.createReadStream(tarPath);
readStream.on('data', () => {
process.stdout.write('\rExtracting...');
});
readStream.pipe(bzip()).pipe(tarStream);
});
}
/**
* Install *.app directory from dmg file
*
* @param {string} dmgPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function installDMG(dmgPath, folderPath) {
let mountPath;
function mountAndCopy(fulfill, reject) {
const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`;
childProcess.exec(mountCommand, (err, stdout) => {
if (err)
return reject(err);
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes)
return reject(new Error(`Could not find volume path in ${stdout}`));
mountPath = volumes[0];
readdirAsync(mountPath)
.then((fileNames) => {
const appName = fileNames.filter((item) => typeof item === 'string' && item.endsWith('.app'))[0];
if (!appName)
return reject(new Error(`Cannot find app in ${mountPath}`));
const copyPath = path.join(mountPath, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => {
if (err)
reject(err);
else
fulfill();
});
})
.catch(reject);
});
}
function unmount() {
if (!mountPath)
return;
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, (err) => {
if (err)
console.error(`Error unmounting dmg: ${err}`);
});
}
return new Promise(mountAndCopy)
.catch((error) => {
console.error(error);
})
.finally(unmount);
}
function httpRequest(url, method, response) {
const urlParsed = URL.parse(url);
let options = {
...urlParsed,
method,
};
const proxyURL = proxy_from_env_1.getProxyForUrl(url);
if (proxyURL) {
if (url.startsWith('http:')) {
const proxy = URL.parse(proxyURL);
options = {
path: options.href,
host: proxy.hostname,
port: proxy.port,
};
}
else {
const parsedProxyURL = URL.parse(proxyURL);
const proxyOptions = {
...parsedProxyURL,
secureProxy: parsedProxyURL.protocol === 'https:',
};
options.agent = new ProxyAgent(proxyOptions);
options.rejectUnauthorized = false;
}
}
const requestCallback = (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
httpRequest(res.headers.location, method, response);
else
response(res);
};
const request = options.protocol === 'https:'
? https.request(options, requestCallback)
: http.request(options, requestCallback);
request.end();
return request;
}
+205
View File
@@ -0,0 +1,205 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CDPSession = exports.Connection = void 0;
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const helper_1 = require("./helper");
const Events_1 = require("./Events");
const debug = require("debug");
const debugProtocol = debug('puppeteer:protocol');
const EventEmitter = require("events");
class Connection extends EventEmitter {
constructor(url, transport, delay = 0) {
super();
this._lastId = 0;
this._sessions = new Map();
this._closed = false;
this._callbacks = new Map();
this._url = url;
this._delay = delay;
this._transport = transport;
this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
}
static fromSession(session) {
return session._connection;
}
/**
* @param {string} sessionId
* @return {?CDPSession}
*/
session(sessionId) {
return this._sessions.get(sessionId) || null;
}
url() {
return this._url;
}
send(method, params) {
const id = this._rawSend({ method, params });
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
_rawSend(message) {
const id = ++this._lastId;
message = JSON.stringify(Object.assign({}, message, { id }));
debugProtocol('SEND ► ' + message);
this._transport.send(message);
return id;
}
async _onMessage(message) {
if (this._delay)
await new Promise((f) => setTimeout(f, this._delay));
debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new CDPSession(this, object.params.targetInfo.type, sessionId);
this._sessions.set(sessionId, session);
}
else if (object.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
if (session) {
session._onClosed();
this._sessions.delete(object.params.sessionId);
}
}
if (object.sessionId) {
const session = this._sessions.get(object.sessionId);
if (session)
session._onMessage(object);
}
else if (object.id) {
const callback = this._callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this._callbacks.delete(object.id);
if (object.error)
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
}
}
else {
this.emit(object.method, object.params);
}
}
_onClose() {
if (this._closed)
return;
this._closed = true;
this._transport.onmessage = null;
this._transport.onclose = null;
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
for (const session of this._sessions.values())
session._onClosed();
this._sessions.clear();
this.emit(Events_1.Events.Connection.Disconnected);
}
dispose() {
this._onClose();
this._transport.close();
}
/**
* @param {Protocol.Target.TargetInfo} targetInfo
* @return {!Promise<!CDPSession>}
*/
async createSession(targetInfo) {
const { sessionId } = await this.send('Target.attachToTarget', {
targetId: targetInfo.targetId,
flatten: true,
});
return this._sessions.get(sessionId);
}
}
exports.Connection = Connection;
class CDPSession extends EventEmitter {
constructor(connection, targetType, sessionId) {
super();
this._callbacks = new Map();
this._connection = connection;
this._targetType = targetType;
this._sessionId = sessionId;
}
send(method, params) {
if (!this._connection)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
const id = this._connection._rawSend({
sessionId: this._sessionId,
method,
/* TODO(jacktfranklin@): once this Firefox bug is solved
* we no longer need the `|| {}` check
* https://bugzilla.mozilla.org/show_bug.cgi?id=1631570
*/
params: params || {},
});
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
_onMessage(object) {
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error)
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
}
else {
helper_1.assert(!object.id);
this.emit(object.method, object.params);
}
}
async detach() {
if (!this._connection)
throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`);
await this._connection.send('Target.detachFromTarget', {
sessionId: this._sessionId,
});
}
_onClosed() {
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
this._connection = null;
this.emit(Events_1.Events.CDPSession.Disconnected);
}
}
exports.CDPSession = CDPSession;
/**
* @param {!Error} error
* @param {string} method
* @param {{error: {message: string, data: any}}} object
* @return {!Error}
*/
function createProtocolError(error, method, object) {
let message = `Protocol error (${method}): ${object.error.message}`;
if ('data' in object.error)
message += ` ${object.error.data}`;
return rewriteError(error, message);
}
/**
* @param {!Error} error
* @param {string} message
* @return {!Error}
*/
function rewriteError(error, message) {
error.message = message;
return error;
}
+17
View File
@@ -0,0 +1,17 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+39
View File
@@ -0,0 +1,39 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsoleMessage = void 0;
class ConsoleMessage {
constructor(type, text, args, location = {}) {
this._type = type;
this._text = text;
this._args = args;
this._location = location;
}
type() {
return this._type;
}
text() {
return this._text;
}
args() {
return this._args;
}
location() {
return this._location;
}
}
exports.ConsoleMessage = ConsoleMessage;
+252
View File
@@ -0,0 +1,252 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Coverage = void 0;
const helper_1 = require("./helper");
const ExecutionContext_1 = require("./ExecutionContext");
class Coverage {
constructor(client) {
this._jsCoverage = new JSCoverage(client);
this._cssCoverage = new CSSCoverage(client);
}
async startJSCoverage(options) {
return await this._jsCoverage.start(options);
}
async stopJSCoverage() {
return await this._jsCoverage.stop();
}
async startCSSCoverage(options) {
return await this._cssCoverage.start(options);
}
async stopCSSCoverage() {
return await this._cssCoverage.stop();
}
}
exports.Coverage = Coverage;
class JSCoverage {
constructor(client) {
this._enabled = false;
this._scriptURLs = new Map();
this._scriptSources = new Map();
this._eventListeners = [];
this._resetOnNavigation = false;
this._reportAnonymousScripts = false;
this._client = client;
}
async start(options = {}) {
helper_1.assert(!this._enabled, 'JSCoverage is already enabled');
const { resetOnNavigation = true, reportAnonymousScripts = false, } = options;
this._resetOnNavigation = resetOnNavigation;
this._reportAnonymousScripts = reportAnonymousScripts;
this._enabled = true;
this._scriptURLs.clear();
this._scriptSources.clear();
this._eventListeners = [
helper_1.helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
];
await Promise.all([
this._client.send('Profiler.enable'),
this._client.send('Profiler.startPreciseCoverage', {
callCount: false,
detailed: true,
}),
this._client.send('Debugger.enable'),
this._client.send('Debugger.setSkipAllPauses', { skip: true }),
]);
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
return;
this._scriptURLs.clear();
this._scriptSources.clear();
}
async _onScriptParsed(event) {
// Ignore puppeteer-injected scripts
if (event.url === ExecutionContext_1.EVALUATION_SCRIPT_URL)
return;
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts)
return;
try {
const response = await this._client.send('Debugger.getScriptSource', {
scriptId: event.scriptId,
});
this._scriptURLs.set(event.scriptId, event.url);
this._scriptSources.set(event.scriptId, response.scriptSource);
}
catch (error) {
// This might happen if the page has already navigated away.
helper_1.debugError(error);
}
}
async stop() {
helper_1.assert(this._enabled, 'JSCoverage is not enabled');
this._enabled = false;
const result = await Promise.all([
this._client.send('Profiler.takePreciseCoverage'),
this._client.send('Profiler.stopPreciseCoverage'),
this._client.send('Profiler.disable'),
this._client.send('Debugger.disable'),
]);
helper_1.helper.removeEventListeners(this._eventListeners);
const coverage = [];
const profileResponse = result[0];
for (const entry of profileResponse.result) {
let url = this._scriptURLs.get(entry.scriptId);
if (!url && this._reportAnonymousScripts)
url = 'debugger://VM' + entry.scriptId;
const text = this._scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined)
continue;
const flattenRanges = [];
for (const func of entry.functions)
flattenRanges.push(...func.ranges);
const ranges = convertToDisjointRanges(flattenRanges);
coverage.push({ url, ranges, text });
}
return coverage;
}
}
class CSSCoverage {
constructor(client) {
this._enabled = false;
this._stylesheetURLs = new Map();
this._stylesheetSources = new Map();
this._eventListeners = [];
this._resetOnNavigation = false;
this._reportAnonymousScripts = false;
this._client = client;
}
async start(options = {}) {
helper_1.assert(!this._enabled, 'CSSCoverage is already enabled');
const { resetOnNavigation = true } = options;
this._resetOnNavigation = resetOnNavigation;
this._enabled = true;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
this._eventListeners = [
helper_1.helper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
helper_1.helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
];
await Promise.all([
this._client.send('DOM.enable'),
this._client.send('CSS.enable'),
this._client.send('CSS.startRuleUsageTracking'),
]);
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
return;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
}
async _onStyleSheet(event) {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL)
return;
try {
const response = await this._client.send('CSS.getStyleSheetText', {
styleSheetId: header.styleSheetId,
});
this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
this._stylesheetSources.set(header.styleSheetId, response.text);
}
catch (error) {
// This might happen if the page has already navigated away.
helper_1.debugError(error);
}
}
async stop() {
helper_1.assert(this._enabled, 'CSSCoverage is not enabled');
this._enabled = false;
const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking');
await Promise.all([
this._client.send('CSS.disable'),
this._client.send('DOM.disable'),
]);
helper_1.helper.removeEventListeners(this._eventListeners);
// aggregate by styleSheetId
const styleSheetIdToCoverage = new Map();
for (const entry of ruleTrackingResponse.ruleUsage) {
let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
if (!ranges) {
ranges = [];
styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
}
ranges.push({
startOffset: entry.startOffset,
endOffset: entry.endOffset,
count: entry.used ? 1 : 0,
});
}
const coverage = [];
for (const styleSheetId of this._stylesheetURLs.keys()) {
const url = this._stylesheetURLs.get(styleSheetId);
const text = this._stylesheetSources.get(styleSheetId);
const ranges = convertToDisjointRanges(styleSheetIdToCoverage.get(styleSheetId) || []);
coverage.push({ url, ranges, text });
}
return coverage;
}
}
function convertToDisjointRanges(nestedRanges) {
const points = [];
for (const range of nestedRanges) {
points.push({ offset: range.startOffset, type: 0, range });
points.push({ offset: range.endOffset, type: 1, range });
}
// Sort points to form a valid parenthesis sequence.
points.sort((a, b) => {
// Sort with increasing offsets.
if (a.offset !== b.offset)
return a.offset - b.offset;
// All "end" points should go before "start" points.
if (a.type !== b.type)
return b.type - a.type;
const aLength = a.range.endOffset - a.range.startOffset;
const bLength = b.range.endOffset - b.range.startOffset;
// For two "start" points, the one with longer range goes first.
if (a.type === 0)
return bLength - aLength;
// For two "end" points, the one with shorter range goes first.
return aLength - bLength;
});
const hitCountStack = [];
const results = [];
let lastOffset = 0;
// Run scanning line to intersect all ranges.
for (const point of points) {
if (hitCountStack.length &&
lastOffset < point.offset &&
hitCountStack[hitCountStack.length - 1] > 0) {
const lastResult = results.length ? results[results.length - 1] : null;
if (lastResult && lastResult.end === lastOffset)
lastResult.end = point.offset;
else
results.push({ start: lastOffset, end: point.offset });
}
lastOffset = point.offset;
if (point.type === 0)
hitCountStack.push(point.range.count);
else
hitCountStack.pop();
}
// Filter out empty ranges.
return results.filter((range) => range.end - range.start > 1);
}
+514
View File
@@ -0,0 +1,514 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOMWorld = void 0;
const fs = require("fs");
const helper_1 = require("./helper");
const LifecycleWatcher_1 = require("./LifecycleWatcher");
const Errors_1 = require("./Errors");
const QueryHandler_1 = require("./QueryHandler");
const readFileAsync = helper_1.helper.promisify(fs.readFile);
class DOMWorld {
constructor(frameManager, frame, timeoutSettings) {
this._documentPromise = null;
this._contextPromise = null;
this._contextResolveCallback = null;
this._detached = false;
this._waitTasks = new Set();
this._frameManager = frameManager;
this._frame = frame;
this._timeoutSettings = timeoutSettings;
this._setContext(null);
}
frame() {
return this._frame;
}
/**
* @param {?ExecutionContext} context
*/
_setContext(context) {
if (context) {
this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null;
for (const waitTask of this._waitTasks)
waitTask.rerun();
}
else {
this._documentPromise = null;
this._contextPromise = new Promise((fulfill) => {
this._contextResolveCallback = fulfill;
});
}
}
_hasContext() {
return !this._contextResolveCallback;
}
_detach() {
this._detached = true;
for (const waitTask of this._waitTasks)
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
}
/**
* @return {!Promise<!ExecutionContext>}
*/
executionContext() {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`);
return this._contextPromise;
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
async evaluate(pageFunction, ...args) {
const context = await this.executionContext();
return context.evaluate(pageFunction, ...args);
}
/**
* @param {string} selector
* @return {!Promise<?ElementHandle>}
*/
async $(selector) {
const document = await this._document();
const value = await document.$(selector);
return value;
}
async _document() {
if (this._documentPromise)
return this._documentPromise;
this._documentPromise = this.executionContext().then(async (context) => {
const document = await context.evaluateHandle('document');
return document.asElement();
});
return this._documentPromise;
}
async $x(expression) {
const document = await this._document();
const value = await document.$x(expression);
return value;
}
async $eval(selector, pageFunction, ...args) {
const document = await this._document();
return document.$eval(selector, pageFunction, ...args);
}
async $$eval(selector, pageFunction, ...args) {
const document = await this._document();
const value = await document.$$eval(selector, pageFunction, ...args);
return value;
}
/**
* @param {string} selector
* @return {!Promise<!Array<!ElementHandle>>}
*/
async $$(selector) {
const document = await this._document();
const value = await document.$$(selector);
return value;
}
async content() {
return await this.evaluate(() => {
let retVal = '';
if (document.doctype)
retVal = new XMLSerializer().serializeToString(document.doctype);
if (document.documentElement)
retVal += document.documentElement.outerHTML;
return retVal;
});
}
async setContent(html, options = {}) {
const { waitUntil = ['load'], timeout = this._timeoutSettings.navigationTimeout(), } = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await this.evaluate((html) => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new LifecycleWatcher_1.LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.lifecyclePromise(),
]);
watcher.dispose();
if (error)
throw error;
}
/**
* @param {!{url?: string, path?: string, content?: string, type?: string}} options
* @return {!Promise<!ElementHandle>}
*/
async addScriptTag(options) {
const { url = null, path = null, content = null, type = '' } = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
}
catch (error) {
throw new Error(`Loading script from ${url} failed`);
}
}
if (path !== null) {
let contents = await readFileAsync(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
}
throw new Error('Provide an object with a `url`, `path` or `content` property');
async function addScriptUrl(url, type) {
const script = document.createElement('script');
script.src = url;
if (type)
script.type = type;
const promise = new Promise((res, rej) => {
script.onload = res;
script.onerror = rej;
});
document.head.appendChild(script);
await promise;
return script;
}
function addScriptContent(content, type = 'text/javascript') {
const script = document.createElement('script');
script.type = type;
script.text = content;
let error = null;
script.onerror = (e) => (error = e);
document.head.appendChild(script);
if (error)
throw error;
return script;
}
}
async addStyleTag(options) {
const { url = null, path = null, content = null } = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleUrl, url)).asElement();
}
catch (error) {
throw new Error(`Loading style from ${url} failed`);
}
}
if (path !== null) {
let contents = await readFileAsync(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleContent, contents)).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleContent, content)).asElement();
}
throw new Error('Provide an object with a `url`, `path` or `content` property');
async function addStyleUrl(url) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
const promise = new Promise((res, rej) => {
link.onload = res;
link.onerror = rej;
});
document.head.appendChild(link);
await promise;
return link;
}
async function addStyleContent(content) {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(content));
const promise = new Promise((res, rej) => {
style.onload = res;
style.onerror = rej;
});
document.head.appendChild(style);
await promise;
return style;
}
}
async click(selector, options) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
await handle.click(options);
await handle.dispose();
}
async focus(selector) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
await handle.focus();
await handle.dispose();
}
async hover(selector) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
await handle.hover();
await handle.dispose();
}
async select(selector, ...values) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
const result = await handle.select(...values);
await handle.dispose();
return result;
}
async tap(selector) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
await handle.tap();
await handle.dispose();
}
async type(selector, text, options) {
const handle = await this.$(selector);
helper_1.assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options);
await handle.dispose();
}
waitForSelector(selector, options) {
return this._waitForSelectorOrXPath(selector, false, options);
}
waitForXPath(xpath, options) {
return this._waitForSelectorOrXPath(xpath, true, options);
}
waitForFunction(pageFunction, options = {}, ...args) {
const { polling = 'raf', timeout = this._timeoutSettings.timeout(), } = options;
return new WaitTask(this, pageFunction, undefined, 'function', polling, timeout, ...args).promise;
}
async title() {
return this.evaluate(() => document.title);
}
async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
const { visible: waitForVisible = false, hidden: waitForHidden = false, timeout = this._timeoutSettings.timeout(), } = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
const { updatedSelector, queryHandler, } = QueryHandler_1.getQueryHandlerAndSelector(selectorOrXPath, (element, selector) => document.querySelector(selector));
const waitTask = new WaitTask(this, predicate, queryHandler, title, polling, timeout, updatedSelector, isXPath, waitForVisible, waitForHidden);
const handle = await waitTask.promise;
if (!handle.asElement()) {
await handle.dispose();
return null;
}
return handle.asElement();
/**
* @param {string} selectorOrXPath
* @param {boolean} isXPath
* @param {boolean} waitForVisible
* @param {boolean} waitForHidden
* @return {?Node|boolean}
*/
function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
const node = isXPath
? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
: predicateQueryHandler
? predicateQueryHandler(document, selectorOrXPath)
: document.querySelector(selectorOrXPath);
if (!node)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return node;
const element = node.nodeType === Node.TEXT_NODE
? node.parentElement
: node;
const style = window.getComputedStyle(element);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success = waitForVisible === isVisible || waitForHidden === !isVisible;
return success ? node : null;
function hasVisibleBoundingBox() {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
}
}
exports.DOMWorld = DOMWorld;
class WaitTask {
constructor(domWorld, predicateBody, predicateQueryHandlerBody, title, polling, timeout, ...args) {
this._runCount = 0;
this._terminated = false;
if (helper_1.helper.isString(polling))
helper_1.assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
else if (helper_1.helper.isNumber(polling))
helper_1.assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
else
throw new Error('Unknown polling options: ' + polling);
function getPredicateBody(predicateBody, predicateQueryHandlerBody) {
if (helper_1.helper.isString(predicateBody))
return `return (${predicateBody});`;
if (predicateQueryHandlerBody) {
return `
return (function wrapper(args) {
const predicateQueryHandler = ${predicateQueryHandlerBody};
return (${predicateBody})(...args);
})(args);`;
}
return `return (${predicateBody})(...args);`;
}
this._domWorld = domWorld;
this._polling = polling;
this._timeout = timeout;
this._predicateBody = getPredicateBody(predicateBody, predicateQueryHandlerBody);
this._args = args;
this._runCount = 0;
domWorld._waitTasks.add(this);
this.promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
if (timeout) {
const timeoutError = new Errors_1.TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
}
this.rerun();
}
terminate(error) {
this._terminated = true;
this._reject(error);
this._cleanup();
}
async rerun() {
const runCount = ++this._runCount;
/** @type {?JSHandle} */
let success = null;
let error = null;
try {
success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
}
catch (error_) {
error = error_;
}
if (this._terminated || runCount !== this._runCount) {
if (success)
await success.dispose();
return;
}
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if (!error &&
(await this._domWorld.evaluate((s) => !s, success).catch(() => true))) {
await success.dispose();
return;
}
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if (error && error.message.includes('Execution context was destroyed'))
return;
// We could have tried to evaluate in a context which was already
// destroyed.
if (error &&
error.message.includes('Cannot find context with specified id'))
return;
if (error)
this._reject(error);
else
this._resolve(success);
this._cleanup();
}
_cleanup() {
clearTimeout(this._timeoutTimer);
this._domWorld._waitTasks.delete(this);
}
}
async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
const predicate = new Function('...args', predicateBody);
let timedOut = false;
if (timeout)
setTimeout(() => (timedOut = true), timeout);
if (polling === 'raf')
return await pollRaf();
if (polling === 'mutation')
return await pollMutation();
if (typeof polling === 'number')
return await pollInterval(polling);
/**
* @return {!Promise<*>}
*/
async function pollMutation() {
const success = await predicate(...args);
if (success)
return Promise.resolve(success);
let fulfill;
const result = new Promise((x) => (fulfill = x));
const observer = new MutationObserver(async () => {
if (timedOut) {
observer.disconnect();
fulfill();
}
const success = await predicate(...args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
observer.observe(document, {
childList: true,
subtree: true,
attributes: true,
});
return result;
}
async function pollRaf() {
let fulfill;
const result = new Promise((x) => (fulfill = x));
await onRaf();
return result;
async function onRaf() {
if (timedOut) {
fulfill();
return;
}
const success = await predicate(...args);
if (success)
fulfill(success);
else
requestAnimationFrame(onRaf);
}
}
async function pollInterval(pollInterval) {
let fulfill;
const result = new Promise((x) => (fulfill = x));
await onTimeout();
return result;
async function onTimeout() {
if (timedOut) {
fulfill();
return;
}
const success = await predicate(...args);
if (success)
fulfill(success);
else
setTimeout(onTimeout, pollInterval);
}
}
}
+876
View File
@@ -0,0 +1,876 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.devicesMap = void 0;
const devices = [
{
name: 'Blackberry PlayBook',
userAgent: 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
viewport: {
width: 600,
height: 1024,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Blackberry PlayBook landscape',
userAgent: 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
viewport: {
width: 1024,
height: 600,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'BlackBerry Z30',
userAgent: 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'BlackBerry Z30 landscape',
userAgent: 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy Note 3',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy Note 3 landscape',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy Note II',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy Note II landscape',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy S III',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy S III landscape',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy S5',
userAgent: 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy S5 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad landscape',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad Mini',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad Mini landscape',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad Pro',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 1366,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad Pro landscape',
userAgent: 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1366,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 4',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
viewport: {
width: 320,
height: 480,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 4 landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
viewport: {
width: 480,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 5',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 320,
height: 568,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 5 landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 568,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 6',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 6 landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 6 Plus',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 6 Plus landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 7',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 7 landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 7 Plus',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 7 Plus landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 8',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 8 landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 8 Plus',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 8 Plus landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone SE',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 320,
height: 568,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone SE landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 568,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone X',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 812,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone X landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 812,
height: 375,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone XR',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
viewport: {
width: 414,
height: 896,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone XR landscape',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
viewport: {
width: 896,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'JioPhone 2',
userAgent: 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
viewport: {
width: 240,
height: 320,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'JioPhone 2 landscape',
userAgent: 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
viewport: {
width: 320,
height: 240,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Kindle Fire HDX',
userAgent: 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
viewport: {
width: 800,
height: 1280,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Kindle Fire HDX landscape',
userAgent: 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
viewport: {
width: 1280,
height: 800,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'LG Optimus L70',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 384,
height: 640,
deviceScaleFactor: 1.25,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'LG Optimus L70 landscape',
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 384,
deviceScaleFactor: 1.25,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Microsoft Lumia 550',
userAgent: 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Microsoft Lumia 950',
userAgent: 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 4,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Microsoft Lumia 950 landscape',
userAgent: 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 4,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 10',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 800,
height: 1280,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 10 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 1280,
height: 800,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 4',
userAgent: 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 384,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 4 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 384,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 5',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 5 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 5X',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 5X landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 6',
userAgent: 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 6 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 6P',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 6P landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 7',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 600,
height: 960,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 7 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 960,
height: 600,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nokia Lumia 520',
userAgent: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
viewport: {
width: 320,
height: 533,
deviceScaleFactor: 1.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nokia Lumia 520 landscape',
userAgent: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
viewport: {
width: 533,
height: 320,
deviceScaleFactor: 1.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nokia N9',
userAgent: 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
viewport: {
width: 480,
height: 854,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nokia N9 landscape',
userAgent: 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
viewport: {
width: 854,
height: 480,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Pixel 2',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 411,
height: 731,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Pixel 2 landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 731,
height: 411,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Pixel 2 XL',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 411,
height: 823,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Pixel 2 XL landscape',
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 823,
height: 411,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
];
const devicesMap = {};
exports.devicesMap = devicesMap;
for (const device of devices)
devicesMap[device.name] = device;
+67
View File
@@ -0,0 +1,67 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Dialog = exports.DialogType = void 0;
const helper_1 = require("./helper");
/* TODO(jacktfranklin): protocol.d.ts defines this
* so let's ditch this and avoid the duplication
*/
var DialogType;
(function (DialogType) {
DialogType["Alert"] = "alert";
DialogType["BeforeUnload"] = "beforeunload";
DialogType["Confirm"] = "confirm";
DialogType["Prompt"] = "prompt";
})(DialogType = exports.DialogType || (exports.DialogType = {}));
let Dialog = /** @class */ (() => {
class Dialog {
constructor(client, type, message, defaultValue = '') {
this._handled = false;
this._client = client;
this._type = type;
this._message = message;
this._defaultValue = defaultValue;
}
type() {
return this._type;
}
message() {
return this._message;
}
defaultValue() {
return this._defaultValue;
}
async accept(promptText) {
helper_1.assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: true,
promptText: promptText,
});
}
async dismiss() {
helper_1.assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: false,
});
}
}
Dialog.Type = DialogType;
return Dialog;
})();
exports.Dialog = Dialog;
+37
View File
@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmulationManager = void 0;
class EmulationManager {
constructor(client) {
this._emulatingMobile = false;
this._hasTouch = false;
this._client = client;
}
async emulateViewport(viewport) {
const mobile = viewport.isMobile || false;
const width = viewport.width;
const height = viewport.height;
const deviceScaleFactor = viewport.deviceScaleFactor || 1;
const screenOrientation = viewport.isLandscape
? { angle: 90, type: 'landscapePrimary' }
: { angle: 0, type: 'portraitPrimary' };
const hasTouch = viewport.hasTouch || false;
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation,
}),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch,
}),
]);
const reloadNeeded = this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
this._emulatingMobile = mobile;
this._hasTouch = hasTouch;
return reloadNeeded;
}
}
exports.EmulationManager = EmulationManager;
+31
View File
@@ -0,0 +1,31 @@
"use strict";
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.puppeteerErrors = exports.TimeoutError = void 0;
class CustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class TimeoutError extends CustomError {
}
exports.TimeoutError = TimeoutError;
exports.puppeteerErrors = {
TimeoutError,
};
+74
View File
@@ -0,0 +1,74 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Events = void 0;
exports.Events = {
Page: {
Close: 'close',
Console: 'console',
Dialog: 'dialog',
DOMContentLoaded: 'domcontentloaded',
Error: 'error',
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: 'pageerror',
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
FrameNavigated: 'framenavigated',
Load: 'load',
Metrics: 'metrics',
Popup: 'popup',
WorkerCreated: 'workercreated',
WorkerDestroyed: 'workerdestroyed',
},
Browser: {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',
Disconnected: 'disconnected',
},
BrowserContext: {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',
},
NetworkManager: {
Request: Symbol('Events.NetworkManager.Request'),
Response: Symbol('Events.NetworkManager.Response'),
RequestFailed: Symbol('Events.NetworkManager.RequestFailed'),
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
},
FrameManager: {
FrameAttached: Symbol('Events.FrameManager.FrameAttached'),
FrameNavigated: Symbol('Events.FrameManager.FrameNavigated'),
FrameDetached: Symbol('Events.FrameManager.FrameDetached'),
LifecycleEvent: Symbol('Events.FrameManager.LifecycleEvent'),
FrameNavigatedWithinDocument: Symbol('Events.FrameManager.FrameNavigatedWithinDocument'),
ExecutionContextCreated: Symbol('Events.FrameManager.ExecutionContextCreated'),
ExecutionContextDestroyed: Symbol('Events.FrameManager.ExecutionContextDestroyed'),
},
Connection: {
Disconnected: Symbol('Events.Connection.Disconnected'),
},
CDPSession: {
Disconnected: Symbol('Events.CDPSession.Disconnected'),
},
};
+174
View File
@@ -0,0 +1,174 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExecutionContext = exports.EVALUATION_SCRIPT_URL = void 0;
const helper_1 = require("./helper");
const JSHandle_1 = require("./JSHandle");
exports.EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
class ExecutionContext {
constructor(client, contextPayload, world) {
this._client = client;
this._world = world;
this._contextId = contextPayload.id;
}
frame() {
return this._world ? this._world.frame() : null;
}
async evaluate(pageFunction, ...args) {
return await this._evaluateInternal(true, pageFunction, ...args);
}
async evaluateHandle(pageFunction, ...args) {
return this._evaluateInternal(false, pageFunction, ...args);
}
async _evaluateInternal(returnByValue, pageFunction, ...args) {
const suffix = `//# sourceURL=${exports.EVALUATION_SCRIPT_URL}`;
if (helper_1.helper.isString(pageFunction)) {
const contextId = this._contextId;
const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression
: expression + '\n' + suffix;
const { exceptionDetails, result: remoteObject } = await this._client
.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue,
awaitPromise: true,
userGesture: true,
})
.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper_1.helper.getExceptionMessage(exceptionDetails));
return returnByValue
? helper_1.helper.valueFromRemoteObject(remoteObject)
: JSHandle_1.createJSHandle(this, remoteObject);
}
if (typeof pageFunction !== 'function')
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
let functionText = pageFunction.toString();
try {
new Function('(' + functionText + ')');
}
catch (error) {
// This means we might have a function shorthand. Try another
// time prefixing 'function '.
if (functionText.startsWith('async '))
functionText =
'async function ' + functionText.substring('async '.length);
else
functionText = 'function ' + functionText;
try {
new Function('(' + functionText + ')');
}
catch (error) {
// We tried hard to serialize, but there's a weird beast here.
throw new Error('Passed function is not well-serializable!');
}
}
let callFunctionOnPromise;
try {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n',
executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)),
returnByValue,
awaitPromise: true,
userGesture: true,
});
}
catch (error) {
if (error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON'))
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
const { exceptionDetails, result: remoteObject, } = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper_1.helper.getExceptionMessage(exceptionDetails));
return returnByValue
? helper_1.helper.valueFromRemoteObject(remoteObject)
: JSHandle_1.createJSHandle(this, remoteObject);
/**
* @param {*} arg
* @return {*}
* @this {ExecutionContext}
*/
function convertArgument(arg) {
if (typeof arg === 'bigint')
// eslint-disable-line valid-typeof
return { unserializableValue: `${arg.toString()}n` };
if (Object.is(arg, -0))
return { unserializableValue: '-0' };
if (Object.is(arg, Infinity))
return { unserializableValue: 'Infinity' };
if (Object.is(arg, -Infinity))
return { unserializableValue: '-Infinity' };
if (Object.is(arg, NaN))
return { unserializableValue: 'NaN' };
const objectHandle = arg && arg instanceof JSHandle_1.JSHandle ? arg : null;
if (objectHandle) {
if (objectHandle._context !== this)
throw new Error('JSHandles can be evaluated only in the context they were created!');
if (objectHandle._disposed)
throw new Error('JSHandle is disposed!');
if (objectHandle._remoteObject.unserializableValue)
return {
unserializableValue: objectHandle._remoteObject.unserializableValue,
};
if (!objectHandle._remoteObject.objectId)
return { value: objectHandle._remoteObject.value };
return { objectId: objectHandle._remoteObject.objectId };
}
return { value: arg };
}
function rewriteError(error) {
if (error.message.includes('Object reference chain is too long'))
return { result: { type: 'undefined' } };
if (error.message.includes("Object couldn't be returned by value"))
return { result: { type: 'undefined' } };
if (error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
}
async queryObjects(prototypeHandle) {
helper_1.assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
helper_1.assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value');
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId,
});
return JSHandle_1.createJSHandle(this, response.objects);
}
async _adoptBackendNodeId(backendNodeId) {
const { object } = await this._client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
executionContextId: this._contextId,
});
return JSHandle_1.createJSHandle(this, object);
}
async _adoptElementHandle(elementHandle) {
helper_1.assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
helper_1.assert(this._world, 'Cannot adopt handle without DOMWorld');
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId);
}
}
exports.ExecutionContext = ExecutionContext;
+39
View File
@@ -0,0 +1,39 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileChooser = void 0;
const helper_1 = require("./helper");
class FileChooser {
constructor(element, event) {
this._handled = false;
this._element = element;
this._multiple = event.mode !== 'selectSingle';
}
isMultiple() {
return this._multiple;
}
async accept(filePaths) {
helper_1.assert(!this._handled, 'Cannot accept FileChooser which is already handled!');
this._handled = true;
await this._element.uploadFile(...filePaths);
}
async cancel() {
helper_1.assert(!this._handled, 'Cannot cancel FileChooser which is already handled!');
this._handled = true;
}
}
exports.FileChooser = FileChooser;
+439
View File
@@ -0,0 +1,439 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Frame = exports.FrameManager = void 0;
const EventEmitter = require("events");
const helper_1 = require("./helper");
const Events_1 = require("./Events");
const ExecutionContext_1 = require("./ExecutionContext");
const LifecycleWatcher_1 = require("./LifecycleWatcher");
const DOMWorld_1 = require("./DOMWorld");
const NetworkManager_1 = require("./NetworkManager");
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
class FrameManager extends EventEmitter {
constructor(client, page, ignoreHTTPSErrors, timeoutSettings) {
super();
this._frames = new Map();
this._contextIdToContext = new Map();
this._isolatedWorlds = new Set();
this._client = client;
this._page = page;
this._networkManager = new NetworkManager_1.NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this._client.on('Page.frameAttached', (event) => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', (event) => this._onFrameNavigated(event.frame));
this._client.on('Page.navigatedWithinDocument', (event) => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Page.frameDetached', (event) => this._onFrameDetached(event.frameId));
this._client.on('Page.frameStoppedLoading', (event) => this._onFrameStoppedLoading(event.frameId));
this._client.on('Runtime.executionContextCreated', (event) => this._onExecutionContextCreated(event.context));
this._client.on('Runtime.executionContextDestroyed', (event) => this._onExecutionContextDestroyed(event.executionContextId));
this._client.on('Runtime.executionContextsCleared', () => this._onExecutionContextsCleared());
this._client.on('Page.lifecycleEvent', (event) => this._onLifecycleEvent(event));
}
async initialize() {
const result = await Promise.all([
this._client.send('Page.enable'),
this._client.send('Page.getFrameTree'),
]);
const { frameTree } = result[1];
this._handleFrameTree(frameTree);
await Promise.all([
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client
.send('Runtime.enable', {})
.then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._networkManager.initialize(),
]);
}
networkManager() {
return this._networkManager;
}
async navigateFrame(frame, url, options = {}) {
assertNoLegacyNavigationOptions(options);
const { referer = this._networkManager.extraHTTPHeaders()['referer'], waitUntil = ['load'], timeout = this._timeoutSettings.navigationTimeout(), } = options;
const watcher = new LifecycleWatcher_1.LifecycleWatcher(this, frame, waitUntil, timeout);
let ensureNewDocumentNavigation = false;
let error = await Promise.race([
navigate(this._client, url, referer, frame._id),
watcher.timeoutOrTerminationPromise(),
]);
if (!error) {
error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
ensureNewDocumentNavigation
? watcher.newDocumentNavigationPromise()
: watcher.sameDocumentNavigationPromise(),
]);
}
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
async function navigate(client, url, referrer, frameId) {
try {
const response = await client.send('Page.navigate', {
url,
referrer,
frameId,
});
ensureNewDocumentNavigation = !!response.loaderId;
return response.errorText
? new Error(`${response.errorText} at ${url}`)
: null;
}
catch (error) {
return error;
}
}
}
async waitForFrameNavigation(frame, options = {}) {
assertNoLegacyNavigationOptions(options);
const { waitUntil = ['load'], timeout = this._timeoutSettings.navigationTimeout(), } = options;
const watcher = new LifecycleWatcher_1.LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.sameDocumentNavigationPromise(),
watcher.newDocumentNavigationPromise(),
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
_onLifecycleEvent(event) {
const frame = this._frames.get(event.frameId);
if (!frame)
return;
frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(Events_1.Events.FrameManager.LifecycleEvent, frame);
}
_onFrameStoppedLoading(frameId) {
const frame = this._frames.get(frameId);
if (!frame)
return;
frame._onLoadingStopped();
this.emit(Events_1.Events.FrameManager.LifecycleEvent, frame);
}
_handleFrameTree(frameTree) {
if (frameTree.frame.parentId)
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
this._onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames)
return;
for (const child of frameTree.childFrames)
this._handleFrameTree(child);
}
page() {
return this._page;
}
mainFrame() {
return this._mainFrame;
}
frames() {
return Array.from(this._frames.values());
}
frame(frameId) {
return this._frames.get(frameId) || null;
}
_onFrameAttached(frameId, parentFrameId) {
if (this._frames.has(frameId))
return;
helper_1.assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new Frame(this, this._client, parentFrame, frameId);
this._frames.set(frame._id, frame);
this.emit(Events_1.Events.FrameManager.FrameAttached, frame);
}
_onFrameNavigated(framePayload) {
const isMainFrame = !framePayload.parentId;
let frame = isMainFrame
? this._mainFrame
: this._frames.get(framePayload.id);
helper_1.assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
// Detach all child frames first.
if (frame) {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
}
// Update or create main frame.
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this._frames.delete(frame._id);
frame._id = framePayload.id;
}
else {
// Initial main frame navigation.
frame = new Frame(this, this._client, null, framePayload.id);
}
this._frames.set(framePayload.id, frame);
this._mainFrame = frame;
}
// Update frame payload.
frame._navigated(framePayload);
this.emit(Events_1.Events.FrameManager.FrameNavigated, frame);
}
async _ensureIsolatedWorld(name) {
if (this._isolatedWorlds.has(name))
return;
this._isolatedWorlds.add(name);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${ExecutionContext_1.EVALUATION_SCRIPT_URL}`,
worldName: name,
}),
await Promise.all(this.frames().map((frame) => this._client
.send('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: name,
})
.catch(helper_1.debugError))); // frames might be removed before we send this
}
_onFrameNavigatedWithinDocument(frameId, url) {
const frame = this._frames.get(frameId);
if (!frame)
return;
frame._navigatedWithinDocument(url);
this.emit(Events_1.Events.FrameManager.FrameNavigatedWithinDocument, frame);
this.emit(Events_1.Events.FrameManager.FrameNavigated, frame);
}
_onFrameDetached(frameId) {
const frame = this._frames.get(frameId);
if (frame)
this._removeFramesRecursively(frame);
}
_onExecutionContextCreated(contextPayload) {
const auxData = contextPayload.auxData;
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
let world = null;
if (frame) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld;
}
else if (contextPayload.name === UTILITY_WORLD_NAME &&
!frame._secondaryWorld._hasContext()) {
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
world = frame._secondaryWorld;
}
}
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context = new ExecutionContext_1.ExecutionContext(this._client, contextPayload, world);
if (world)
world._setContext(context);
this._contextIdToContext.set(contextPayload.id, context);
}
/**
* @param {number} executionContextId
*/
_onExecutionContextDestroyed(executionContextId) {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
return;
this._contextIdToContext.delete(executionContextId);
if (context._world)
context._world._setContext(null);
}
_onExecutionContextsCleared() {
for (const context of this._contextIdToContext.values()) {
if (context._world)
context._world._setContext(null);
}
this._contextIdToContext.clear();
}
executionContextById(contextId) {
const context = this._contextIdToContext.get(contextId);
helper_1.assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
return context;
}
_removeFramesRecursively(frame) {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
frame._detach();
this._frames.delete(frame._id);
this.emit(Events_1.Events.FrameManager.FrameDetached, frame);
}
}
exports.FrameManager = FrameManager;
class Frame {
constructor(frameManager, client, parentFrame, frameId) {
this._url = '';
this._detached = false;
this._loaderId = '';
this._lifecycleEvents = new Set();
this._frameManager = frameManager;
this._client = client;
this._parentFrame = parentFrame;
this._url = '';
this._id = frameId;
this._detached = false;
this._loaderId = '';
this._mainWorld = new DOMWorld_1.DOMWorld(frameManager, this, frameManager._timeoutSettings);
this._secondaryWorld = new DOMWorld_1.DOMWorld(frameManager, this, frameManager._timeoutSettings);
this._childFrames = new Set();
if (this._parentFrame)
this._parentFrame._childFrames.add(this);
}
async goto(url, options) {
return await this._frameManager.navigateFrame(this, url, options);
}
async waitForNavigation(options) {
return await this._frameManager.waitForFrameNavigation(this, options);
}
executionContext() {
return this._mainWorld.executionContext();
}
async evaluateHandle(pageFunction, ...args) {
return this._mainWorld.evaluateHandle(pageFunction, ...args);
}
async evaluate(pageFunction, ...args) {
return this._mainWorld.evaluate(pageFunction, ...args);
}
async $(selector) {
return this._mainWorld.$(selector);
}
async $x(expression) {
return this._mainWorld.$x(expression);
}
async $eval(selector, pageFunction, ...args) {
return this._mainWorld.$eval(selector, pageFunction, ...args);
}
async $$eval(selector, pageFunction, ...args) {
return this._mainWorld.$$eval(selector, pageFunction, ...args);
}
async $$(selector) {
return this._mainWorld.$$(selector);
}
async content() {
return this._secondaryWorld.content();
}
async setContent(html, options = {}) {
return this._secondaryWorld.setContent(html, options);
}
name() {
return this._name || '';
}
url() {
return this._url;
}
parentFrame() {
return this._parentFrame;
}
childFrames() {
return Array.from(this._childFrames);
}
isDetached() {
return this._detached;
}
async addScriptTag(options) {
return this._mainWorld.addScriptTag(options);
}
async addStyleTag(options) {
return this._mainWorld.addStyleTag(options);
}
async click(selector, options) {
return this._secondaryWorld.click(selector, options);
}
async focus(selector) {
return this._secondaryWorld.focus(selector);
}
async hover(selector) {
return this._secondaryWorld.hover(selector);
}
select(selector, ...values) {
return this._secondaryWorld.select(selector, ...values);
}
async tap(selector) {
return this._secondaryWorld.tap(selector);
}
async type(selector, text, options) {
return this._mainWorld.type(selector, text, options);
}
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
const xPathPattern = '//';
if (helper_1.helper.isString(selectorOrFunctionOrTimeout)) {
const string = selectorOrFunctionOrTimeout;
if (string.startsWith(xPathPattern))
return this.waitForXPath(string, options);
return this.waitForSelector(string, options);
}
if (helper_1.helper.isNumber(selectorOrFunctionOrTimeout))
return new Promise((fulfill) => setTimeout(fulfill, selectorOrFunctionOrTimeout));
if (typeof selectorOrFunctionOrTimeout === 'function')
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
return Promise.reject(new Error('Unsupported target type: ' + typeof selectorOrFunctionOrTimeout));
}
async waitForSelector(selector, options) {
const handle = await this._secondaryWorld.waitForSelector(selector, options);
if (!handle)
return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
async waitForXPath(xpath, options) {
const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle)
return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
waitForFunction(pageFunction, options = {}, ...args) {
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
}
async title() {
return this._secondaryWorld.title();
}
_navigated(framePayload) {
this._name = framePayload.name;
this._url = framePayload.url;
}
_navigatedWithinDocument(url) {
this._url = url;
}
_onLifecycleEvent(loaderId, name) {
if (name === 'init') {
this._loaderId = loaderId;
this._lifecycleEvents.clear();
}
this._lifecycleEvents.add(name);
}
_onLoadingStopped() {
this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load');
}
_detach() {
this._detached = true;
this._mainWorld._detach();
this._secondaryWorld._detach();
if (this._parentFrame)
this._parentFrame._childFrames.delete(this);
this._parentFrame = null;
}
}
exports.Frame = Frame;
function assertNoLegacyNavigationOptions(options) {
helper_1.assert(options['networkIdleTimeout'] === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');
helper_1.assert(options['networkIdleInflight'] === undefined, 'ERROR: networkIdleInflight option is no longer supported.');
helper_1.assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead');
}
+230
View File
@@ -0,0 +1,230 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTTPRequest = void 0;
const helper_1 = require("./helper");
class HTTPRequest {
constructor(client, frame, interceptionId, allowInterception, event, redirectChain) {
this._failureText = null;
this._response = null;
this._fromMemoryCache = false;
this._interceptionHandled = false;
this._headers = {};
this._client = client;
this._requestId = event.requestId;
this._isNavigationRequest =
event.requestId === event.loaderId && event.type === 'Document';
this._interceptionId = interceptionId;
this._allowInterception = allowInterception;
this._url = event.request.url;
this._resourceType = event.type.toLowerCase();
this._method = event.request.method;
this._postData = event.request.postData;
this._frame = frame;
this._redirectChain = redirectChain;
for (const key of Object.keys(event.request.headers))
this._headers[key.toLowerCase()] = event.request.headers[key];
}
url() {
return this._url;
}
resourceType() {
return this._resourceType;
}
method() {
return this._method;
}
postData() {
return this._postData;
}
headers() {
return this._headers;
}
response() {
return this._response;
}
frame() {
return this._frame;
}
isNavigationRequest() {
return this._isNavigationRequest;
}
redirectChain() {
return this._redirectChain.slice();
}
/**
* @return {?{errorText: string}}
*/
failure() {
if (!this._failureText)
return null;
return {
errorText: this._failureText,
};
}
async continue(overrides = {}) {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:'))
return;
helper_1.assert(this._allowInterception, 'Request Interception is not enabled!');
helper_1.assert(!this._interceptionHandled, 'Request is already handled!');
const { url, method, postData, headers } = overrides;
this._interceptionHandled = true;
await this._client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData,
headers: headers ? headersArray(headers) : undefined,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
helper_1.debugError(error);
});
}
async respond(response) {
// Mocking responses for dataURL requests is not currently supported.
if (this._url.startsWith('data:'))
return;
helper_1.assert(this._allowInterception, 'Request Interception is not enabled!');
helper_1.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
const responseBody = response.body && helper_1.helper.isString(response.body)
? Buffer.from(response.body)
: response.body || null;
const responseHeaders = {};
if (response.headers) {
for (const header of Object.keys(response.headers))
responseHeaders[header.toLowerCase()] = response.headers[header];
}
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
await this._client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: response.status || 200,
responsePhrase: STATUS_TEXTS[response.status || 200],
responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
helper_1.debugError(error);
});
}
async abort(errorCode = 'failed') {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:'))
return;
const errorReason = errorReasons[errorCode];
helper_1.assert(errorReason, 'Unknown error code: ' + errorCode);
helper_1.assert(this._allowInterception, 'Request Interception is not enabled!');
helper_1.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
await this._client
.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
helper_1.debugError(error);
});
}
}
exports.HTTPRequest = HTTPRequest;
const errorReasons = {
aborted: 'Aborted',
accessdenied: 'AccessDenied',
addressunreachable: 'AddressUnreachable',
blockedbyclient: 'BlockedByClient',
blockedbyresponse: 'BlockedByResponse',
connectionaborted: 'ConnectionAborted',
connectionclosed: 'ConnectionClosed',
connectionfailed: 'ConnectionFailed',
connectionrefused: 'ConnectionRefused',
connectionreset: 'ConnectionReset',
internetdisconnected: 'InternetDisconnected',
namenotresolved: 'NameNotResolved',
timedout: 'TimedOut',
failed: 'Failed',
};
function headersArray(headers) {
const result = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined))
result.push({ name, value: headers[name] + '' });
}
return result;
}
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
const STATUS_TEXTS = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
};
+87
View File
@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTTPResponse = void 0;
const SecurityDetails_1 = require("./SecurityDetails");
class HTTPResponse {
constructor(client, request, responsePayload) {
this._contentPromise = null;
this._headers = {};
this._client = client;
this._request = request;
this._bodyLoadedPromise = new Promise((fulfill) => {
this._bodyLoadedPromiseFulfill = fulfill;
});
this._remoteAddress = {
ip: responsePayload.remoteIPAddress,
port: responsePayload.remotePort,
};
this._status = responsePayload.status;
this._statusText = responsePayload.statusText;
this._url = request.url();
this._fromDiskCache = !!responsePayload.fromDiskCache;
this._fromServiceWorker = !!responsePayload.fromServiceWorker;
for (const key of Object.keys(responsePayload.headers))
this._headers[key.toLowerCase()] = responsePayload.headers[key];
this._securityDetails = responsePayload.securityDetails
? new SecurityDetails_1.SecurityDetails(responsePayload.securityDetails)
: null;
}
_resolveBody(err) {
return this._bodyLoadedPromiseFulfill(err);
}
remoteAddress() {
return this._remoteAddress;
}
url() {
return this._url;
}
ok() {
return this._status === 0 || (this._status >= 200 && this._status <= 299);
}
status() {
return this._status;
}
statusText() {
return this._statusText;
}
headers() {
return this._headers;
}
securityDetails() {
return this._securityDetails;
}
buffer() {
if (!this._contentPromise) {
this._contentPromise = this._bodyLoadedPromise.then(async (error) => {
if (error)
throw error;
const response = await this._client.send('Network.getResponseBody', {
requestId: this._request._requestId,
});
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
});
}
return this._contentPromise;
}
async text() {
const content = await this.buffer();
return content.toString('utf8');
}
async json() {
const content = await this.text();
return JSON.parse(content);
}
request() {
return this._request;
}
fromCache() {
return this._fromDiskCache || this._request._fromMemoryCache;
}
fromServiceWorker() {
return this._fromServiceWorker;
}
frame() {
return this._request.frame();
}
}
exports.HTTPResponse = HTTPResponse;
+233
View File
@@ -0,0 +1,233 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Touchscreen = exports.Mouse = exports.Keyboard = void 0;
const helper_1 = require("./helper");
const USKeyboardLayout_1 = require("./USKeyboardLayout");
class Keyboard {
constructor(client) {
this._modifiers = 0;
this._pressedKeys = new Set();
this._client = client;
}
async down(key, options = { text: undefined }) {
const description = this._keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text;
await this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
key: description.key,
text: text,
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3,
});
}
_modifierBit(key) {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Meta')
return 4;
if (key === 'Shift')
return 8;
return 0;
}
_keyDescriptionForString(keyString) {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0,
};
const definition = USKeyboardLayout_1.keyDefinitions[keyString];
helper_1.assert(definition, `Unknown key: "${keyString}"`);
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8)
description.text = '';
return description;
}
async up(key) {
const description = this._keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
this._pressedKeys.delete(description.code);
await this._client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this._modifiers,
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location,
});
}
async sendCharacter(char) {
await this._client.send('Input.insertText', { text: char });
}
charIsKey(char) {
return !!USKeyboardLayout_1.keyDefinitions[char];
}
async type(text, options) {
const delay = (options && options.delay) || null;
for (const char of text) {
if (this.charIsKey(char)) {
await this.press(char, { delay });
}
else {
if (delay)
await new Promise((f) => setTimeout(f, delay));
await this.sendCharacter(char);
}
}
}
async press(key, options = {}) {
const { delay = null } = options;
await this.down(key, options);
if (delay)
await new Promise((f) => setTimeout(f, options.delay));
await this.up(key);
}
}
exports.Keyboard = Keyboard;
class Mouse {
/**
* @param {CDPSession} client
* @param {!Keyboard} keyboard
*/
constructor(client, keyboard) {
this._x = 0;
this._y = 0;
this._button = 'none';
this._client = client;
this._keyboard = keyboard;
}
async move(x, y, options = {}) {
const { steps = 1 } = options;
const fromX = this._x, fromY = this._y;
this._x = x;
this._y = y;
for (let i = 1; i <= steps; i++) {
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers,
});
}
}
async click(x, y, options = {}) {
const { delay = null } = options;
if (delay !== null) {
await Promise.all([this.move(x, y), this.down(options)]);
await new Promise((f) => setTimeout(f, delay));
await this.up(options);
}
else {
await Promise.all([
this.move(x, y),
this.down(options),
this.up(options),
]);
}
}
async down(options = {}) {
const { button = 'left', clickCount = 1 } = options;
this._button = button;
await this._client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
button,
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount,
});
}
/**
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async up(options = {}) {
const { button = 'left', clickCount = 1 } = options;
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button,
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount,
});
}
}
exports.Mouse = Mouse;
class Touchscreen {
constructor(client, keyboard) {
this._client = client;
this._keyboard = keyboard;
}
/**
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
// Touches appear to be lost during the first frame after navigation.
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
await this._client.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true,
});
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints,
modifiers: this._keyboard._modifiers,
});
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
modifiers: this._keyboard._modifiers,
});
}
}
exports.Touchscreen = Touchscreen;
+456
View File
@@ -0,0 +1,456 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElementHandle = exports.JSHandle = exports.createJSHandle = void 0;
const helper_1 = require("./helper");
const QueryHandler_1 = require("./QueryHandler");
function createJSHandle(context, remoteObject) {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._frameManager;
return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager);
}
return new JSHandle(context, context._client, remoteObject);
}
exports.createJSHandle = createJSHandle;
class JSHandle {
constructor(context, client, remoteObject) {
this._disposed = false;
this._context = context;
this._client = client;
this._remoteObject = remoteObject;
}
executionContext() {
return this._context;
}
async evaluate(pageFunction, ...args) {
return await this.executionContext().evaluate(pageFunction, this, ...args);
}
async evaluateHandle(pageFunction, ...args) {
return await this.executionContext().evaluateHandle(pageFunction, this, ...args);
}
async getProperty(propertyName) {
const objectHandle = await this.evaluateHandle((object, propertyName) => {
const result = { __proto__: null };
result[propertyName] = object[propertyName];
return result;
}, propertyName);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
return result;
}
async getProperties() {
const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId,
ownProperties: true,
});
const result = new Map();
for (const property of response.result) {
if (!property.enumerable)
continue;
result.set(property.name, createJSHandle(this._context, property.value));
}
return result;
}
async jsonValue() {
if (this._remoteObject.objectId) {
const response = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }',
objectId: this._remoteObject.objectId,
returnByValue: true,
awaitPromise: true,
});
return helper_1.helper.valueFromRemoteObject(response.result);
}
return helper_1.helper.valueFromRemoteObject(this._remoteObject);
}
/* This always returns null but children can define this and return an ElementHandle */
asElement() {
return null;
}
async dispose() {
if (this._disposed)
return;
this._disposed = true;
await helper_1.helper.releaseObject(this._client, this._remoteObject);
}
toString() {
if (this._remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type;
return 'JSHandle@' + type;
}
return 'JSHandle:' + helper_1.helper.valueFromRemoteObject(this._remoteObject);
}
}
exports.JSHandle = JSHandle;
class ElementHandle extends JSHandle {
constructor(context, client, remoteObject, page, frameManager) {
super(context, client, remoteObject);
this._client = client;
this._remoteObject = remoteObject;
this._page = page;
this._frameManager = frameManager;
}
asElement() {
return this;
}
async contentFrame() {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: this._remoteObject.objectId,
});
if (typeof nodeInfo.node.frameId !== 'string')
return null;
return this._frameManager.frame(nodeInfo.node.frameId);
}
async _scrollIntoViewIfNeeded() {
const error = await this.evaluate(async (element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
return false;
}
const visibleRatio = await new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
if (visibleRatio !== 1.0) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
}
return false;
}, this._page._javascriptEnabled);
if (error)
throw new Error(error);
}
async _clickablePoint() {
const [result, layoutMetrics] = await Promise.all([
this._client
.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId,
})
.catch(helper_1.debugError),
this._client.send('Page.getLayoutMetrics'),
]);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into.
const { clientWidth, clientHeight } = layoutMetrics.layoutViewport;
const quads = result.quads
.map((quad) => this._fromProtocolQuad(quad))
.map((quad) => this._intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
const quad = quads[0];
let x = 0;
let y = 0;
for (const point of quad) {
x += point.x;
y += point.y;
}
return {
x: x / 4,
y: y / 4,
};
}
_getBoxModel() {
return this._client
.send('DOM.getBoxModel', {
objectId: this._remoteObject.objectId,
})
.catch((error) => helper_1.debugError(error));
}
_fromProtocolQuad(quad) {
return [
{ x: quad[0], y: quad[1] },
{ x: quad[2], y: quad[3] },
{ x: quad[4], y: quad[5] },
{ x: quad[6], y: quad[7] },
];
}
_intersectQuadWithViewport(quad, width, height) {
return quad.map((point) => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
}
async hover() {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.mouse.move(x, y);
}
async click(options) {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.mouse.click(x, y, options);
}
async select(...values) {
for (const value of values)
helper_1.assert(helper_1.helper.isString(value), 'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"');
/* TODO(jacktfranklin@): once ExecutionContext is TypeScript, and
* its evaluate function is properly typed with generics we can
* return here and remove the typecasting
*/
return this.evaluate((element, values) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return options
.filter((option) => option.selected)
.map((option) => option.value);
}, values);
}
async uploadFile(...filePaths) {
const isMultiple = await this.evaluate((element) => element.multiple);
helper_1.assert(filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with <input type=file multiple>');
// This import is only needed for `uploadFile`, so keep it scoped here to avoid paying
// the cost unnecessarily.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { promisify } = require('util');
const access = promisify(fs.access);
// Locate all files and confirm that they exist.
const files = await Promise.all(filePaths.map(async (filePath) => {
const resolvedPath = path.resolve(filePath);
try {
await access(resolvedPath, fs.constants.R_OK);
}
catch (error) {
if (error.code === 'ENOENT')
throw new Error(`${filePath} does not exist or is not readable`);
}
return resolvedPath;
}));
const { objectId } = this._remoteObject;
const { node } = await this._client.send('DOM.describeNode', { objectId });
const { backendNodeId } = node;
// The zero-length array is a special case, it seems that DOM.setFileInputFiles does
// not actually update the files in that case, so the solution is to eval the element
// value to a new FileList directly.
if (files.length === 0) {
await this.evaluate((element) => {
element.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
});
}
else {
await this._client.send('DOM.setFileInputFiles', {
objectId,
files,
backendNodeId,
});
}
}
async tap() {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.touchscreen.tap(x, y);
}
async focus() {
await this.evaluate((element) => element.focus());
}
async type(text, options) {
await this.focus();
await this._page.keyboard.type(text, options);
}
async press(key, options) {
await this.focus();
await this._page.keyboard.press(key, options);
}
async boundingBox() {
const result = await this._getBoxModel();
if (!result)
return null;
const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
return { x, y, width, height };
}
/**
* @return {!Promise<?BoxModel>}
*/
async boxModel() {
const result = await this._getBoxModel();
if (!result)
return null;
const { content, padding, border, margin, width, height } = result.model;
return {
content: this._fromProtocolQuad(content),
padding: this._fromProtocolQuad(padding),
border: this._fromProtocolQuad(border),
margin: this._fromProtocolQuad(margin),
width,
height,
};
}
async screenshot(options = {}) {
let needsViewportReset = false;
let boundingBox = await this.boundingBox();
helper_1.assert(boundingBox, 'Node is either not visible or not an HTMLElement');
const viewport = this._page.viewport();
if (viewport &&
(boundingBox.width > viewport.width ||
boundingBox.height > viewport.height)) {
const newViewport = {
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
};
await this._page.setViewport(Object.assign({}, viewport, newViewport));
needsViewportReset = true;
}
await this._scrollIntoViewIfNeeded();
boundingBox = await this.boundingBox();
helper_1.assert(boundingBox, 'Node is either not visible or not an HTMLElement');
helper_1.assert(boundingBox.width !== 0, 'Node has 0 width.');
helper_1.assert(boundingBox.height !== 0, 'Node has 0 height.');
const { layoutViewport: { pageX, pageY }, } = await this._client.send('Page.getLayoutMetrics');
const clip = Object.assign({}, boundingBox);
clip.x += pageX;
clip.y += pageY;
const imageData = await this._page.screenshot(Object.assign({}, {
clip,
}, options));
if (needsViewportReset)
await this._page.setViewport(viewport);
return imageData;
}
async $(selector) {
const defaultHandler = (element, selector) => element.querySelector(selector);
const { updatedSelector, queryHandler } = QueryHandler_1.getQueryHandlerAndSelector(selector, defaultHandler);
const handle = await this.evaluateHandle(queryHandler, updatedSelector);
const element = handle.asElement();
if (element)
return element;
await handle.dispose();
return null;
}
async $$(selector) {
const defaultHandler = (element, selector) => element.querySelectorAll(selector);
const { updatedSelector, queryHandler } = QueryHandler_1.getQueryHandlerAndSelector(selector, defaultHandler);
const arrayHandle = await this.evaluateHandle(queryHandler, updatedSelector);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
async $eval(selector, pageFunction, ...args) {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await elementHandle.evaluate(pageFunction, ...args);
await elementHandle.dispose();
return result;
}
async $$eval(selector, pageFunction, ...args) {
const defaultHandler = (element, selector) => Array.from(element.querySelectorAll(selector));
const { updatedSelector, queryHandler } = QueryHandler_1.getQueryHandlerAndSelector(selector, defaultHandler);
const arrayHandle = await this.evaluateHandle(queryHandler, updatedSelector);
const result = await arrayHandle.evaluate(pageFunction, ...args);
await arrayHandle.dispose();
return result;
}
async $x(expression) {
const arrayHandle = await this.evaluateHandle((element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
const array = [];
let item;
while ((item = iterator.iterateNext()))
array.push(item);
return array;
}, expression);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
async isIntersectingViewport() {
return await this.evaluate(async (element) => {
const visibleRatio = await new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
return visibleRatio > 0;
});
}
}
exports.ElementHandle = ElementHandle;
function computeQuadArea(quad) {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
for (let i = 0; i < quad.length; ++i) {
const p1 = quad[i];
const p2 = quad[(i + 1) % quad.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
}
+530
View File
@@ -0,0 +1,530 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require("os");
const path = require("path");
const http = require("http");
const https = require("https");
const URL = require("url");
const fs = require("fs");
const BrowserFetcher_1 = require("./BrowserFetcher");
const Connection_1 = require("./Connection");
const Browser_1 = require("./Browser");
const helper_1 = require("./helper");
const WebSocketTransport_1 = require("./WebSocketTransport");
const BrowserRunner_1 = require("./launcher/BrowserRunner");
const mkdtempAsync = helper_1.helper.promisify(fs.mkdtemp);
const writeFileAsync = helper_1.helper.promisify(fs.writeFile);
class ChromeLauncher {
constructor(projectRoot, preferredRevision, isPuppeteerCore) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options = {}) {
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, pipe = false, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, slowMo = 0, timeout = 30000, } = options;
const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
const chromeArguments = [];
if (!ignoreDefaultArgs)
chromeArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
chromeArguments.push(...this.defaultArgs(options).filter((arg) => !ignoreDefaultArgs.includes(arg)));
else
chromeArguments.push(...args);
let temporaryUserDataDir = null;
if (!chromeArguments.some((argument) => argument.startsWith('--remote-debugging-')))
chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
if (!chromeArguments.some((arg) => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(profilePath);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
}
let chromeExecutable = executablePath;
if (!executablePath) {
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner_1.BrowserRunner(chromeExecutable, chromeArguments, temporaryUserDataDir);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
});
try {
const connection = await runner.setupConnection({
usePipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser_1.Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
}
catch (error) {
runner.kill();
throw error;
}
}
/**
* @param {!Launcher.ChromeArgOptions=} options
* @return {!Array<string>}
*/
defaultArgs(options = {}) {
const chromeArguments = [
'--disable-background-networking',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=TranslateUI',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
];
const { devtools = false, headless = !devtools, args = [], userDataDir = null, } = options;
if (userDataDir)
chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (headless) {
chromeArguments.push('--headless', '--hide-scrollbars', '--mute-audio');
}
if (args.every((arg) => arg.startsWith('-')))
chromeArguments.push('about:blank');
chromeArguments.push(...args);
return chromeArguments;
}
executablePath() {
return resolveExecutablePath(this).executablePath;
}
get product() {
return 'chrome';
}
async connect(options) {
const { browserWSEndpoint, browserURL, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, transport, slowMo = 0, } = options;
helper_1.assert(Number(!!browserWSEndpoint) +
Number(!!browserURL) +
Number(!!transport) ===
1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
let connection = null;
if (transport) {
connection = new Connection_1.Connection('', transport, slowMo);
}
else if (browserWSEndpoint) {
const connectionTransport = await WebSocketTransport_1.WebSocketTransport.create(browserWSEndpoint);
connection = new Connection_1.Connection(browserWSEndpoint, connectionTransport, slowMo);
}
else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const connectionTransport = await WebSocketTransport_1.WebSocketTransport.create(connectionURL);
connection = new Connection_1.Connection(connectionURL, connectionTransport, slowMo);
}
const { browserContextIds } = await connection.send('Target.getBrowserContexts');
return Browser_1.Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(helper_1.debugError));
}
}
class FirefoxLauncher {
constructor(projectRoot, preferredRevision, isPuppeteerCore) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options = {}) {
const { ignoreDefaultArgs = false, args = [], dumpio = false, executablePath = null, pipe = false, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, slowMo = 0, timeout = 30000, extraPrefsFirefox = {}, } = options;
const firefoxArguments = [];
if (!ignoreDefaultArgs)
firefoxArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
firefoxArguments.push(...this.defaultArgs(options).filter((arg) => !ignoreDefaultArgs.includes(arg)));
else
firefoxArguments.push(...args);
if (!firefoxArguments.some((argument) => argument.startsWith('--remote-debugging-')))
firefoxArguments.push('--remote-debugging-port=0');
let temporaryUserDataDir = null;
if (!firefoxArguments.includes('-profile') &&
!firefoxArguments.includes('--profile')) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
}
await this._updateRevision();
let firefoxExecutable = executablePath;
if (!executablePath) {
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
firefoxExecutable = executablePath;
}
const runner = new BrowserRunner_1.BrowserRunner(firefoxExecutable, firefoxArguments, temporaryUserDataDir);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe,
});
try {
const connection = await runner.setupConnection({
usePipe: pipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser_1.Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
}
catch (error) {
runner.kill();
throw error;
}
}
async connect(options) {
const { browserWSEndpoint, browserURL, ignoreHTTPSErrors = false, defaultViewport = { width: 800, height: 600 }, transport, slowMo = 0, } = options;
helper_1.assert(Number(!!browserWSEndpoint) +
Number(!!browserURL) +
Number(!!transport) ===
1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
let connection = null;
if (transport) {
connection = new Connection_1.Connection('', transport, slowMo);
}
else if (browserWSEndpoint) {
const connectionTransport = await WebSocketTransport_1.WebSocketTransport.create(browserWSEndpoint);
connection = new Connection_1.Connection(browserWSEndpoint, connectionTransport, slowMo);
}
else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const connectionTransport = await WebSocketTransport_1.WebSocketTransport.create(connectionURL);
connection = new Connection_1.Connection(connectionURL, connectionTransport, slowMo);
}
const { browserContextIds } = await connection.send('Target.getBrowserContexts');
return Browser_1.Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(helper_1.debugError));
}
executablePath() {
return resolveExecutablePath(this).executablePath;
}
async _updateRevision() {
// replace 'latest' placeholder with actual downloaded revision
if (this._preferredRevision === 'latest') {
const browserFetcher = new BrowserFetcher_1.BrowserFetcher(this._projectRoot, {
product: this.product,
});
const localRevisions = await browserFetcher.localRevisions();
if (localRevisions[0])
this._preferredRevision = localRevisions[0];
}
}
get product() {
return 'firefox';
}
defaultArgs(options = {}) {
const firefoxArguments = ['--no-remote', '--foreground'];
const { devtools = false, headless = !devtools, args = [], userDataDir = null, } = options;
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
if (headless)
firefoxArguments.push('--headless');
if (devtools)
firefoxArguments.push('--devtools');
if (args.every((arg) => arg.startsWith('-')))
firefoxArguments.push('about:blank');
firefoxArguments.push(...args);
return firefoxArguments;
}
async _createProfile(extraPrefs) {
const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-'));
const prefsJS = [];
const userJS = [];
const server = 'dummy.test';
const defaultPreferences = {
// Make sure Shield doesn't hit the network.
'app.normandy.api_url': '',
// Disable Firefox old build background check
'app.update.checkInstallTime': false,
// Disable automatically upgrading Firefox
'app.update.disabledForTesting': true,
// Increase the APZ content response timeout to 1 minute
'apz.content_response_timeout': 60000,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
// Enable the dump function: which sends messages to the system
// console
// https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
'browser.dom.window.dump.enabled': true,
// Disable topstories
'browser.newtabpage.activity-stream.feeds.section.topstories': false,
// Always display a blank page
'browser.newtabpage.enabled': false,
// Background thumbnails in particular cause grief: and disabling
// thumbnails in general cannot hurt
'browser.pagethumbnails.capturing_disabled': true,
// Disable safebrowsing components.
'browser.safebrowsing.blockedURIs.enabled': false,
'browser.safebrowsing.downloads.enabled': false,
'browser.safebrowsing.malware.enabled': false,
'browser.safebrowsing.passwords.enabled': false,
'browser.safebrowsing.phishing.enabled': false,
// Disable updates to search engines.
'browser.search.update': false,
// Do not restore the last open set of tabs if the browser has crashed
'browser.sessionstore.resume_from_crash': false,
// Skip check for default browser on startup
'browser.shell.checkDefaultBrowser': false,
// Disable newtabpage
'browser.startup.homepage': 'about:blank',
// Do not redirect user when a milstone upgrade of Firefox is detected
'browser.startup.homepage_override.mstone': 'ignore',
// Start with a blank page about:blank
'browser.startup.page': 0,
// Do not allow background tabs to be zombified on Android: otherwise for
// tests that open additional tabs: the test harness tab itself might get
// unloaded
'browser.tabs.disableBackgroundZombification': false,
// Do not warn when closing all other open tabs
'browser.tabs.warnOnCloseOtherTabs': false,
// Do not warn when multiple tabs will be opened
'browser.tabs.warnOnOpen': false,
// Disable the UI tour.
'browser.uitour.enabled': false,
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
'browser.urlbar.suggest.searches': false,
// Disable first run splash page on Windows 10
'browser.usedOnWindows10.introURL': '',
// Do not warn on quitting Firefox
'browser.warnOnQuit': false,
// Do not show datareporting policy notifications which can
// interfere with tests
'datareporting.healthreport.about.reportUrl': `http://${server}/dummy/abouthealthreport/`,
'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
'datareporting.healthreport.logging.consoleEnabled': false,
'datareporting.healthreport.service.enabled': false,
'datareporting.healthreport.service.firstRun': false,
'datareporting.healthreport.uploadEnabled': false,
'datareporting.policy.dataSubmissionEnabled': false,
'datareporting.policy.dataSubmissionPolicyAccepted': false,
'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
// This doesn't affect Puppeteer but spams console (Bug 1424372)
'devtools.jsonview.enabled': false,
// Disable popup-blocker
'dom.disable_open_during_load': false,
// Enable the support for File object creation in the content process
// Required for |Page.setFileInputFiles| protocol method.
'dom.file.createInChild': true,
// Disable the ProcessHangMonitor
'dom.ipc.reportProcessHangs': false,
// Disable slow script dialogues
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
'extensions.enabledScopes': 5,
// Disable metadata caching for installed add-ons by default
'extensions.getAddons.cache.enabled': false,
// Disable installing any distribution extensions or add-ons.
'extensions.installDistroAddons': false,
// Disabled screenshots extension
'extensions.screenshots.disabled': true,
// Turn off extension updates so they do not bother tests
'extensions.update.enabled': false,
// Turn off extension updates so they do not bother tests
'extensions.update.notifyUser': false,
// Make sure opening about:addons will not hit the network
'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
// Allow the application to have focus even it runs in the background
'focusmanager.testmode': true,
// Disable useragent updates
'general.useragent.updates.enabled': false,
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
'geo.provider.testing': true,
// Do not scan Wifi
'geo.wifi.scan': false,
// No hang monitor
'hangmonitor.timeout': 0,
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'network.cookie.cookieBehavior': 0,
// Do not prompt for temporary redirects
'network.http.prompt-temp-redirect': false,
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
'network.http.speculative-parallel-limit': 0,
// Do not automatically switch between offline and online
'network.manage-offline-status': false,
// Make sure SNTP requests do not hit the network
'network.sntp.pools': server,
// Disable Flash.
'plugin.state.flash': 0,
'privacy.trackingprotection.enabled': false,
// Enable Remote Agent
// https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
'remote.enabled': true,
// Don't do network connections for mitm priming
'security.certerrors.mitm.priming.enabled': false,
// Local documents have access to all other local documents,
// including directory listings
'security.fileuri.strict_origin_policy': false,
// Do not wait for the notification button security delay
'security.notification_enable_delay': 0,
// Ensure blocklist updates do not hit the network
'services.settings.server': `http://${server}/dummy/blocklist/`,
// Do not automatically fill sign-in forms with known usernames and
// passwords
'signon.autofillForms': false,
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
'signon.rememberSignons': false,
// Disable first-run welcome page
'startup.homepage_welcome_url': 'about:blank',
// Disable first-run welcome page
'startup.homepage_welcome_url.additional': '',
// Disable browser animations (tabs, fullscreen, sliding alerts)
'toolkit.cosmeticAnimations.enabled': false,
// We want to collect telemetry, but we don't want to send in the results
'toolkit.telemetry.server': `https://${server}/dummy/telemetry/`,
// Prevent starting into safe mode after application crashes
'toolkit.startup.max_resumed_crashes': -1,
};
Object.assign(defaultPreferences, extraPrefs);
for (const [key, value] of Object.entries(defaultPreferences))
userJS.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
await writeFileAsync(path.join(profilePath, 'prefs.js'), prefsJS.join('\n'));
return profilePath;
}
}
function getWSEndpoint(browserURL) {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const endpointURL = URL.resolve(browserURL, '/json/version');
const protocol = endpointURL.startsWith('https') ? https : http;
const requestOptions = Object.assign(URL.parse(endpointURL), {
method: 'GET',
});
const request = protocol.request(requestOptions, (res) => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
res.resume();
reject(new Error('HTTP ' + res.statusCode));
return;
}
res.setEncoding('utf8');
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl));
});
request.on('error', reject);
request.end();
return promise.catch((error) => {
error.message =
`Failed to fetch browser webSocket url from ${endpointURL}: ` +
error.message;
throw error;
});
}
function resolveExecutablePath(launcher) {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!launcher._isPuppeteerCore) {
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.npm_config_puppeteer_executable_path ||
process.env.npm_package_config_puppeteer_executable_path;
if (executablePath) {
const missingText = !fs.existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: null;
return { executablePath, missingText };
}
}
const browserFetcher = new BrowserFetcher_1.BrowserFetcher(launcher._projectRoot, {
product: launcher.product,
});
if (!launcher._isPuppeteerCore && launcher.product === 'chrome') {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local
? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' +
revisionInfo.executablePath
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
}
const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
const missingText = !revisionInfo.local
? `Could not find browser revision ${launcher._preferredRevision}. Run "npm install" or "yarn install" to download a browser binary.`
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
function Launcher(projectRoot, preferredRevision, isPuppeteerCore, product) {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore)
product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
switch (product) {
case 'firefox':
return new FirefoxLauncher(projectRoot, preferredRevision, isPuppeteerCore);
case 'chrome':
default:
if (typeof product !== 'undefined' && product !== 'chrome') {
/* The user gave us an incorrect product name
* we'll default to launching Chrome, but log to the console
* to let the user know (they've probably typoed).
*/
console.warn(`Warning: unknown product name ${product}. Falling back to chrome.`);
}
return new ChromeLauncher(projectRoot, preferredRevision, isPuppeteerCore);
}
}
exports.default = Launcher;
+142
View File
@@ -0,0 +1,142 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.LifecycleWatcher = void 0;
const helper_1 = require("./helper");
const Events_1 = require("./Events");
const Errors_1 = require("./Errors");
const puppeteerToProtocolLifecycle = new Map([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);
class LifecycleWatcher {
constructor(frameManager, frame, waitUntil, timeout) {
if (Array.isArray(waitUntil))
waitUntil = waitUntil.slice();
else if (typeof waitUntil === 'string')
waitUntil = [waitUntil];
this._expectedLifecycle = waitUntil.map((value) => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
helper_1.assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent;
});
this._frameManager = frameManager;
this._frame = frame;
this._initialLoaderId = frame._loaderId;
this._timeout = timeout;
this._navigationRequest = null;
this._eventListeners = [
helper_1.helper.addEventListener(frameManager._client, Events_1.Events.CDPSession.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
helper_1.helper.addEventListener(this._frameManager, Events_1.Events.FrameManager.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
helper_1.helper.addEventListener(this._frameManager, Events_1.Events.FrameManager.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
helper_1.helper.addEventListener(this._frameManager, Events_1.Events.FrameManager.FrameDetached, this._onFrameDetached.bind(this)),
helper_1.helper.addEventListener(this._frameManager.networkManager(), Events_1.Events.NetworkManager.Request, this._onRequest.bind(this)),
];
this._sameDocumentNavigationPromise = new Promise((fulfill) => {
this._sameDocumentNavigationCompleteCallback = fulfill;
});
this._lifecyclePromise = new Promise((fulfill) => {
this._lifecycleCallback = fulfill;
});
this._newDocumentNavigationPromise = new Promise((fulfill) => {
this._newDocumentNavigationCompleteCallback = fulfill;
});
this._timeoutPromise = this._createTimeoutPromise();
this._terminationPromise = new Promise((fulfill) => {
this._terminationCallback = fulfill;
});
this._checkLifecycleComplete();
}
_onRequest(request) {
if (request.frame() !== this._frame || !request.isNavigationRequest())
return;
this._navigationRequest = request;
}
_onFrameDetached(frame) {
if (this._frame === frame) {
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
return;
}
this._checkLifecycleComplete();
}
navigationResponse() {
return this._navigationRequest ? this._navigationRequest.response() : null;
}
_terminate(error) {
this._terminationCallback.call(null, error);
}
sameDocumentNavigationPromise() {
return this._sameDocumentNavigationPromise;
}
newDocumentNavigationPromise() {
return this._newDocumentNavigationPromise;
}
lifecyclePromise() {
return this._lifecyclePromise;
}
timeoutOrTerminationPromise() {
return Promise.race([this._timeoutPromise, this._terminationPromise]);
}
_createTimeoutPromise() {
if (!this._timeout)
return new Promise(() => { });
const errorMessage = 'Navigation timeout of ' + this._timeout + ' ms exceeded';
return new Promise((fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout))).then(() => new Errors_1.TimeoutError(errorMessage));
}
_navigatedWithinDocument(frame) {
if (frame !== this._frame)
return;
this._hasSameDocumentNavigation = true;
this._checkLifecycleComplete();
}
_checkLifecycleComplete() {
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle))
return;
this._lifecycleCallback();
if (this._frame._loaderId === this._initialLoaderId &&
!this._hasSameDocumentNavigation)
return;
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
if (this._frame._loaderId !== this._initialLoaderId)
this._newDocumentNavigationCompleteCallback();
/**
* @param {!Frame} frame
* @param {!Array<string>} expectedLifecycle
* @return {boolean}
*/
function checkLifecycle(frame, expectedLifecycle) {
for (const event of expectedLifecycle) {
if (!frame._lifecycleEvents.has(event))
return false;
}
for (const child of frame.childFrames()) {
if (!checkLifecycle(child, expectedLifecycle))
return false;
}
return true;
}
}
dispose() {
helper_1.helper.removeEventListeners(this._eventListeners);
clearTimeout(this._maximumTimer);
}
}
exports.LifecycleWatcher = LifecycleWatcher;
+252
View File
@@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkManager = void 0;
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const EventEmitter = require("events");
const helper_1 = require("./helper");
const Events_1 = require("./Events");
const HTTPRequest_1 = require("./HTTPRequest");
const HTTPResponse_1 = require("./HTTPResponse");
class NetworkManager extends EventEmitter {
constructor(client, ignoreHTTPSErrors, frameManager) {
super();
this._requestIdToRequest = new Map();
this._requestIdToRequestWillBeSentEvent = new Map();
this._extraHTTPHeaders = {};
this._offline = false;
this._credentials = null;
this._attemptedAuthentications = new Set();
this._userRequestInterceptionEnabled = false;
this._protocolRequestInterceptionEnabled = false;
this._userCacheDisabled = false;
this._requestIdToInterceptionId = new Map();
this._client = client;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._frameManager = frameManager;
this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this));
this._client.on('Network.requestServedFromCache', this._onRequestServedFromCache.bind(this));
this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
}
async initialize() {
await this._client.send('Network.enable');
if (this._ignoreHTTPSErrors)
await this._client.send('Security.setIgnoreCertificateErrors', {
ignore: true,
});
}
async authenticate(credentials) {
this._credentials = credentials;
await this._updateProtocolRequestInterception();
}
async setExtraHTTPHeaders(extraHTTPHeaders) {
this._extraHTTPHeaders = {};
for (const key of Object.keys(extraHTTPHeaders)) {
const value = extraHTTPHeaders[key];
helper_1.assert(helper_1.helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
this._extraHTTPHeaders[key.toLowerCase()] = value;
}
await this._client.send('Network.setExtraHTTPHeaders', {
headers: this._extraHTTPHeaders,
});
}
extraHTTPHeaders() {
return Object.assign({}, this._extraHTTPHeaders);
}
async setOfflineMode(value) {
if (this._offline === value)
return;
this._offline = value;
await this._client.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
}
async setUserAgent(userAgent) {
await this._client.send('Network.setUserAgentOverride', { userAgent });
}
async setCacheEnabled(enabled) {
this._userCacheDisabled = !enabled;
await this._updateProtocolCacheDisabled();
}
async setRequestInterception(value) {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
}
async _updateProtocolRequestInterception() {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{ urlPattern: '*' }],
}),
]);
}
else {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.disable'),
]);
}
}
async _updateProtocolCacheDisabled() {
await this._client.send('Network.setCacheDisabled', {
cacheDisabled: this._userCacheDisabled || this._protocolRequestInterceptionEnabled,
});
}
_onRequestWillBeSent(event) {
// Request interception doesn't happen for data URLs with Network Service.
if (this._protocolRequestInterceptionEnabled &&
!event.request.url.startsWith('data:')) {
const requestId = event.requestId;
const interceptionId = this._requestIdToInterceptionId.get(requestId);
if (interceptionId) {
this._onRequest(event, interceptionId);
this._requestIdToInterceptionId.delete(requestId);
}
else {
this._requestIdToRequestWillBeSentEvent.set(event.requestId, event);
}
return;
}
this._onRequest(event, null);
}
/**
* @param {!Protocol.Fetch.authRequiredPayload} event
*/
_onAuthRequired(event) {
let response = 'Default';
if (this._attemptedAuthentications.has(event.requestId)) {
response = 'CancelAuth';
}
else if (this._credentials) {
response = 'ProvideCredentials';
this._attemptedAuthentications.add(event.requestId);
}
const { username, password } = this._credentials || {
username: undefined,
password: undefined,
};
this._client
.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
})
.catch(helper_1.debugError);
}
_onRequestPaused(event) {
if (!this._userRequestInterceptionEnabled &&
this._protocolRequestInterceptionEnabled) {
this._client
.send('Fetch.continueRequest', {
requestId: event.requestId,
})
.catch(helper_1.debugError);
}
const requestId = event.networkId;
const interceptionId = event.requestId;
if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
this._onRequest(requestWillBeSentEvent, interceptionId);
this._requestIdToRequestWillBeSentEvent.delete(requestId);
}
else {
this._requestIdToInterceptionId.set(requestId, interceptionId);
}
}
_onRequest(event, interceptionId) {
let redirectChain = [];
if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) {
this._handleRequestRedirect(request, event.redirectResponse);
redirectChain = request._redirectChain;
}
}
const frame = event.frameId
? this._frameManager.frame(event.frameId)
: null;
const request = new HTTPRequest_1.HTTPRequest(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
this._requestIdToRequest.set(event.requestId, request);
this.emit(Events_1.Events.NetworkManager.Request, request);
}
_onRequestServedFromCache(event) {
const request = this._requestIdToRequest.get(event.requestId);
if (request)
request._fromMemoryCache = true;
}
_handleRequestRedirect(request, responsePayload) {
const response = new HTTPResponse_1.HTTPResponse(this._client, request, responsePayload);
request._response = response;
request._redirectChain.push(request);
response._resolveBody(new Error('Response body is unavailable for redirect responses'));
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events_1.Events.NetworkManager.Response, response);
this.emit(Events_1.Events.NetworkManager.RequestFinished, request);
}
_onResponseReceived(event) {
const request = this._requestIdToRequest.get(event.requestId);
// FileUpload sends a response without a matching request.
if (!request)
return;
const response = new HTTPResponse_1.HTTPResponse(this._client, request, event.response);
request._response = response;
this.emit(Events_1.Events.NetworkManager.Response, response);
}
_onLoadingFinished(event) {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
if (request.response())
request.response()._resolveBody(null);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events_1.Events.NetworkManager.RequestFinished, request);
}
_onLoadingFailed(event) {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
request._failureText = event.errorText;
const response = request.response();
if (response)
response._resolveBody(null);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events_1.Events.NetworkManager.RequestFailed, request);
}
}
exports.NetworkManager = NetworkManager;
+859
View File
@@ -0,0 +1,859 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Page = void 0;
const fs = require("fs");
const EventEmitter = require("events");
const mime = require("mime");
const Events_1 = require("./Events");
const Connection_1 = require("./Connection");
const Dialog_1 = require("./Dialog");
const EmulationManager_1 = require("./EmulationManager");
const FrameManager_1 = require("./FrameManager");
const Input_1 = require("./Input");
const Tracing_1 = require("./Tracing");
const helper_1 = require("./helper");
const Coverage_1 = require("./Coverage");
const WebWorker_1 = require("./WebWorker");
const JSHandle_1 = require("./JSHandle");
const Accessibility_1 = require("./Accessibility");
const TimeoutSettings_1 = require("./TimeoutSettings");
const FileChooser_1 = require("./FileChooser");
const ConsoleMessage_1 = require("./ConsoleMessage");
const writeFileAsync = helper_1.helper.promisify(fs.writeFile);
const paperFormats = {
letter: { width: 8.5, height: 11 },
legal: { width: 8.5, height: 14 },
tabloid: { width: 11, height: 17 },
ledger: { width: 17, height: 11 },
a0: { width: 33.1, height: 46.8 },
a1: { width: 23.4, height: 33.1 },
a2: { width: 16.54, height: 23.4 },
a3: { width: 11.7, height: 16.54 },
a4: { width: 8.27, height: 11.7 },
a5: { width: 5.83, height: 8.27 },
a6: { width: 4.13, height: 5.83 },
};
var VisionDeficiency;
(function (VisionDeficiency) {
VisionDeficiency["none"] = "none";
VisionDeficiency["achromatopsia"] = "achromatopsia";
VisionDeficiency["blurredVision"] = "blurredVision";
VisionDeficiency["deuteranopia"] = "deuteranopia";
VisionDeficiency["protanopia"] = "protanopia";
VisionDeficiency["tritanopia"] = "tritanopia";
})(VisionDeficiency || (VisionDeficiency = {}));
class ScreenshotTaskQueue {
constructor() {
this._chain = Promise.resolve(undefined);
}
postTask(task) {
const result = this._chain.then(task);
this._chain = result.catch(() => { });
return result;
}
}
class Page extends EventEmitter {
constructor(client, target, ignoreHTTPSErrors) {
super();
this._closed = false;
this._timeoutSettings = new TimeoutSettings_1.TimeoutSettings();
this._pageBindings = new Map();
this._javascriptEnabled = true;
this._workers = new Map();
// TODO: improve this typedef - it's a function that takes a file chooser or something?
this._fileChooserInterceptors = new Set();
this._client = client;
this._target = target;
this._keyboard = new Input_1.Keyboard(client);
this._mouse = new Input_1.Mouse(client, this._keyboard);
this._touchscreen = new Input_1.Touchscreen(client, this._keyboard);
this._accessibility = new Accessibility_1.Accessibility(client);
this._frameManager = new FrameManager_1.FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
this._emulationManager = new EmulationManager_1.EmulationManager(client);
this._tracing = new Tracing_1.Tracing(client);
this._coverage = new Coverage_1.Coverage(client);
this._screenshotTaskQueue = new ScreenshotTaskQueue();
this._viewport = null;
client.on('Target.attachedToTarget', (event) => {
if (event.targetInfo.type !== 'worker') {
// If we don't detach from service workers, they will never die.
client
.send('Target.detachFromTarget', {
sessionId: event.sessionId,
})
.catch(helper_1.debugError);
return;
}
const session = Connection_1.Connection.fromSession(client).session(event.sessionId);
const worker = new WebWorker_1.WebWorker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
this._workers.set(event.sessionId, worker);
this.emit(Events_1.Events.Page.WorkerCreated, worker);
});
client.on('Target.detachedFromTarget', (event) => {
const worker = this._workers.get(event.sessionId);
if (!worker)
return;
this.emit(Events_1.Events.Page.WorkerDestroyed, worker);
this._workers.delete(event.sessionId);
});
this._frameManager.on(Events_1.Events.FrameManager.FrameAttached, (event) => this.emit(Events_1.Events.Page.FrameAttached, event));
this._frameManager.on(Events_1.Events.FrameManager.FrameDetached, (event) => this.emit(Events_1.Events.Page.FrameDetached, event));
this._frameManager.on(Events_1.Events.FrameManager.FrameNavigated, (event) => this.emit(Events_1.Events.Page.FrameNavigated, event));
const networkManager = this._frameManager.networkManager();
networkManager.on(Events_1.Events.NetworkManager.Request, (event) => this.emit(Events_1.Events.Page.Request, event));
networkManager.on(Events_1.Events.NetworkManager.Response, (event) => this.emit(Events_1.Events.Page.Response, event));
networkManager.on(Events_1.Events.NetworkManager.RequestFailed, (event) => this.emit(Events_1.Events.Page.RequestFailed, event));
networkManager.on(Events_1.Events.NetworkManager.RequestFinished, (event) => this.emit(Events_1.Events.Page.RequestFinished, event));
this._fileChooserInterceptors = new Set();
client.on('Page.domContentEventFired', () => this.emit(Events_1.Events.Page.DOMContentLoaded));
client.on('Page.loadEventFired', () => this.emit(Events_1.Events.Page.Load));
client.on('Runtime.consoleAPICalled', (event) => this._onConsoleAPI(event));
client.on('Runtime.bindingCalled', (event) => this._onBindingCalled(event));
client.on('Page.javascriptDialogOpening', (event) => this._onDialog(event));
client.on('Runtime.exceptionThrown', (exception) => this._handleException(exception.exceptionDetails));
client.on('Inspector.targetCrashed', () => this._onTargetCrashed());
client.on('Performance.metrics', (event) => this._emitMetrics(event));
client.on('Log.entryAdded', (event) => this._onLogEntryAdded(event));
client.on('Page.fileChooserOpened', (event) => this._onFileChooser(event));
this._target._isClosedPromise.then(() => {
this.emit(Events_1.Events.Page.Close);
this._closed = true;
});
}
static async create(client, target, ignoreHTTPSErrors, defaultViewport) {
const page = new Page(client, target, ignoreHTTPSErrors);
await page._initialize();
if (defaultViewport)
await page.setViewport(defaultViewport);
return page;
}
async _initialize() {
await Promise.all([
this._frameManager.initialize(),
this._client.send('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: false,
flatten: true,
}),
this._client.send('Performance.enable', {}),
this._client.send('Log.enable', {}),
]);
}
async _onFileChooser(event) {
if (!this._fileChooserInterceptors.size)
return;
const frame = this._frameManager.frame(event.frameId);
const context = await frame.executionContext();
const element = await context._adoptBackendNodeId(event.backendNodeId);
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const fileChooser = new FileChooser_1.FileChooser(element, event);
for (const interceptor of interceptors)
interceptor.call(null, fileChooser);
}
async waitForFileChooser(options = {}) {
if (!this._fileChooserInterceptors.size)
await this._client.send('Page.setInterceptFileChooserDialog', {
enabled: true,
});
const { timeout = this._timeoutSettings.timeout() } = options;
let callback;
const promise = new Promise((x) => (callback = x));
this._fileChooserInterceptors.add(callback);
return helper_1.helper
.waitWithTimeout(promise, 'waiting for file chooser', timeout)
.catch((error) => {
this._fileChooserInterceptors.delete(callback);
throw error;
});
}
async setGeolocation(options) {
const { longitude, latitude, accuracy = 0 } = options;
if (longitude < -180 || longitude > 180)
throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`);
if (latitude < -90 || latitude > 90)
throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`);
if (accuracy < 0)
throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`);
await this._client.send('Emulation.setGeolocationOverride', {
longitude,
latitude,
accuracy,
});
}
target() {
return this._target;
}
browser() {
return this._target.browser();
}
browserContext() {
return this._target.browserContext();
}
_onTargetCrashed() {
this.emit('error', new Error('Page crashed!'));
}
_onLogEntryAdded(event) {
const { level, text, args, source, url, lineNumber } = event.entry;
if (args)
args.map((arg) => helper_1.helper.releaseObject(this._client, arg));
if (source !== 'worker')
this.emit(Events_1.Events.Page.Console, new ConsoleMessage_1.ConsoleMessage(level, text, [], { url, lineNumber }));
}
mainFrame() {
return this._frameManager.mainFrame();
}
get keyboard() {
return this._keyboard;
}
get touchscreen() {
return this._touchscreen;
}
get coverage() {
return this._coverage;
}
get tracing() {
return this._tracing;
}
get accessibility() {
return this._accessibility;
}
frames() {
return this._frameManager.frames();
}
workers() {
return Array.from(this._workers.values());
}
async setRequestInterception(value) {
return this._frameManager.networkManager().setRequestInterception(value);
}
setOfflineMode(enabled) {
return this._frameManager.networkManager().setOfflineMode(enabled);
}
setDefaultNavigationTimeout(timeout) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector) {
return this.mainFrame().$(selector);
}
async evaluateHandle(pageFunction, ...args) {
const context = await this.mainFrame().executionContext();
return context.evaluateHandle(pageFunction, ...args);
}
async queryObjects(prototypeHandle) {
const context = await this.mainFrame().executionContext();
return context.queryObjects(prototypeHandle);
}
async $eval(selector, pageFunction, ...args) {
return this.mainFrame().$eval(selector, pageFunction, ...args);
}
async $$eval(selector, pageFunction, ...args) {
return this.mainFrame().$$eval(selector, pageFunction, ...args);
}
async $$(selector) {
return this.mainFrame().$$(selector);
}
async $x(expression) {
return this.mainFrame().$x(expression);
}
async cookies(...urls) {
const originalCookies = (await this._client.send('Network.getCookies', {
urls: urls.length ? urls : [this.url()],
})).cookies;
const unsupportedCookieAttributes = ['priority'];
const filterUnsupportedAttributes = (cookie) => {
for (const attr of unsupportedCookieAttributes)
delete cookie[attr];
return cookie;
};
return originalCookies.map(filterUnsupportedAttributes);
}
async deleteCookie(...cookies) {
const pageURL = this.url();
for (const cookie of cookies) {
const item = Object.assign({}, cookie);
if (!cookie.url && pageURL.startsWith('http'))
item.url = pageURL;
await this._client.send('Network.deleteCookies', item);
}
}
async setCookie(...cookies) {
const pageURL = this.url();
const startsWithHTTP = pageURL.startsWith('http');
const items = cookies.map((cookie) => {
const item = Object.assign({}, cookie);
if (!item.url && startsWithHTTP)
item.url = pageURL;
helper_1.assert(item.url !== 'about:blank', `Blank page can not have cookie "${item.name}"`);
helper_1.assert(!String.prototype.startsWith.call(item.url || '', 'data:'), `Data URL page can not have cookie "${item.name}"`);
return item;
});
await this.deleteCookie(...items);
if (items.length)
await this._client.send('Network.setCookies', { cookies: items });
}
async addScriptTag(options) {
return this.mainFrame().addScriptTag(options);
}
async addStyleTag(options) {
return this.mainFrame().addStyleTag(options);
}
async exposeFunction(name, puppeteerFunction) {
if (this._pageBindings.has(name))
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
this._pageBindings.set(name, puppeteerFunction);
const expression = helper_1.helper.evaluationString(addPageBinding, name);
await this._client.send('Runtime.addBinding', { name: name });
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: expression,
});
await Promise.all(this.frames().map((frame) => frame.evaluate(expression).catch(helper_1.debugError)));
function addPageBinding(bindingName) {
/* Cast window to any here as we're about to add properties to it
* via win[bindingName] which TypeScript doesn't like.
*/
const win = window;
const binding = win[bindingName];
win[bindingName] = (...args) => {
const me = window[bindingName];
let callbacks = me['callbacks'];
if (!callbacks) {
callbacks = new Map();
me['callbacks'] = callbacks;
}
const seq = (me['lastSeq'] || 0) + 1;
me['lastSeq'] = seq;
const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }));
binding(JSON.stringify({ name: bindingName, seq, args }));
return promise;
};
}
}
async authenticate(credentials) {
return this._frameManager.networkManager().authenticate(credentials);
}
async setExtraHTTPHeaders(headers) {
return this._frameManager.networkManager().setExtraHTTPHeaders(headers);
}
async setUserAgent(userAgent) {
return this._frameManager.networkManager().setUserAgent(userAgent);
}
async metrics() {
const response = await this._client.send('Performance.getMetrics');
return this._buildMetricsObject(response.metrics);
}
_emitMetrics(event) {
this.emit(Events_1.Events.Page.Metrics, {
title: event.title,
metrics: this._buildMetricsObject(event.metrics),
});
}
_buildMetricsObject(metrics) {
const result = {};
for (const metric of metrics || []) {
if (supportedMetrics.has(metric.name))
result[metric.name] = metric.value;
}
return result;
}
_handleException(exceptionDetails) {
const message = helper_1.helper.getExceptionMessage(exceptionDetails);
const err = new Error(message);
err.stack = ''; // Don't report clientside error with a node stack attached
this.emit(Events_1.Events.Page.PageError, err);
}
async _onConsoleAPI(event) {
if (event.executionContextId === 0) {
// DevTools protocol stores the last 1000 console messages. These
// messages are always reported even for removed execution contexts. In
// this case, they are marked with executionContextId = 0 and are
// reported upon enabling Runtime agent.
//
// Ignore these messages since:
// - there's no execution context we can use to operate with message
// arguments
// - these messages are reported before Puppeteer clients can subscribe
// to the 'console'
// page event.
//
// @see https://github.com/puppeteer/puppeteer/issues/3865
return;
}
const context = this._frameManager.executionContextById(event.executionContextId);
const values = event.args.map((arg) => JSHandle_1.createJSHandle(context, arg));
this._addConsoleMessage(event.type, values, event.stackTrace);
}
async _onBindingCalled(event) {
const { name, seq, args } = JSON.parse(event.payload);
let expression = null;
try {
const result = await this._pageBindings.get(name)(...args);
expression = helper_1.helper.evaluationString(deliverResult, name, seq, result);
}
catch (error) {
if (error instanceof Error)
expression = helper_1.helper.evaluationString(deliverError, name, seq, error.message, error.stack);
else
expression = helper_1.helper.evaluationString(deliverErrorValue, name, seq, error);
}
this._client
.send('Runtime.evaluate', {
expression,
contextId: event.executionContextId,
})
.catch(helper_1.debugError);
function deliverResult(name, seq, result) {
window[name]['callbacks'].get(seq).resolve(result);
window[name]['callbacks'].delete(seq);
}
function deliverError(name, seq, message, stack) {
const error = new Error(message);
error.stack = stack;
window[name]['callbacks'].get(seq).reject(error);
window[name]['callbacks'].delete(seq);
}
function deliverErrorValue(name, seq, value) {
window[name]['callbacks'].get(seq).reject(value);
window[name]['callbacks'].delete(seq);
}
}
_addConsoleMessage(type, args, stackTrace) {
if (!this.listenerCount(Events_1.Events.Page.Console)) {
args.forEach((arg) => arg.dispose());
return;
}
const textTokens = [];
for (const arg of args) {
const remoteObject = arg._remoteObject;
if (remoteObject.objectId)
textTokens.push(arg.toString());
else
textTokens.push(helper_1.helper.valueFromRemoteObject(remoteObject));
}
const location = stackTrace && stackTrace.callFrames.length
? {
url: stackTrace.callFrames[0].url,
lineNumber: stackTrace.callFrames[0].lineNumber,
columnNumber: stackTrace.callFrames[0].columnNumber,
}
: {};
const message = new ConsoleMessage_1.ConsoleMessage(type, textTokens.join(' '), args, location);
this.emit(Events_1.Events.Page.Console, message);
}
_onDialog(event) {
let dialogType = null;
if (event.type === 'alert')
dialogType = Dialog_1.Dialog.Type.Alert;
else if (event.type === 'confirm')
dialogType = Dialog_1.Dialog.Type.Confirm;
else if (event.type === 'prompt')
dialogType = Dialog_1.Dialog.Type.Prompt;
else if (event.type === 'beforeunload')
dialogType = Dialog_1.Dialog.Type.BeforeUnload;
helper_1.assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
const dialog = new Dialog_1.Dialog(this._client, dialogType, event.message, event.defaultPrompt);
this.emit(Events_1.Events.Page.Dialog, dialog);
}
url() {
return this.mainFrame().url();
}
async content() {
return await this._frameManager.mainFrame().content();
}
async setContent(html, options) {
await this._frameManager.mainFrame().setContent(html, options);
}
async goto(url, options) {
return await this._frameManager.mainFrame().goto(url, options);
}
async reload(options) {
const result = await Promise.all([this.waitForNavigation(options), this._client.send('Page.reload')]);
return result[0];
}
async waitForNavigation(options = {}) {
return await this._frameManager.mainFrame().waitForNavigation(options);
}
_sessionClosePromise() {
if (!this._disconnectPromise)
this._disconnectPromise = new Promise((fulfill) => this._client.once(Events_1.Events.CDPSession.Disconnected, () => fulfill(new Error('Target closed'))));
return this._disconnectPromise;
}
async waitForRequest(urlOrPredicate, options = {}) {
const { timeout = this._timeoutSettings.timeout() } = options;
return helper_1.helper.waitForEvent(this._frameManager.networkManager(), Events_1.Events.NetworkManager.Request, (request) => {
if (helper_1.helper.isString(urlOrPredicate))
return urlOrPredicate === request.url();
if (typeof urlOrPredicate === 'function')
return !!urlOrPredicate(request);
return false;
}, timeout, this._sessionClosePromise());
}
async waitForResponse(urlOrPredicate, options = {}) {
const { timeout = this._timeoutSettings.timeout() } = options;
return helper_1.helper.waitForEvent(this._frameManager.networkManager(), Events_1.Events.NetworkManager.Response, (response) => {
if (helper_1.helper.isString(urlOrPredicate))
return urlOrPredicate === response.url();
if (typeof urlOrPredicate === 'function')
return !!urlOrPredicate(response);
return false;
}, timeout, this._sessionClosePromise());
}
async goBack(options) {
return this._go(-1, options);
}
async goForward(options) {
return this._go(+1, options);
}
async _go(delta, options) {
const history = await this._client.send('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta];
if (!entry)
return null;
const result = await Promise.all([
this.waitForNavigation(options),
this._client.send('Page.navigateToHistoryEntry', { entryId: entry.id }),
]);
return result[0];
}
async bringToFront() {
await this._client.send('Page.bringToFront');
}
async emulate(options) {
await Promise.all([
this.setViewport(options.viewport),
this.setUserAgent(options.userAgent),
]);
}
async setJavaScriptEnabled(enabled) {
if (this._javascriptEnabled === enabled)
return;
this._javascriptEnabled = enabled;
await this._client.send('Emulation.setScriptExecutionDisabled', {
value: !enabled,
});
}
async setBypassCSP(enabled) {
await this._client.send('Page.setBypassCSP', { enabled });
}
async emulateMediaType(type) {
helper_1.assert(type === 'screen' || type === 'print' || type === null, 'Unsupported media type: ' + type);
await this._client.send('Emulation.setEmulatedMedia', {
media: type || '',
});
}
async emulateMediaFeatures(features) {
if (features === null)
await this._client.send('Emulation.setEmulatedMedia', { features: null });
if (Array.isArray(features)) {
features.every((mediaFeature) => {
const name = mediaFeature.name;
helper_1.assert(/^prefers-(?:color-scheme|reduced-motion)$/.test(name), 'Unsupported media feature: ' + name);
return true;
});
await this._client.send('Emulation.setEmulatedMedia', {
features: features,
});
}
}
async emulateTimezone(timezoneId) {
try {
await this._client.send('Emulation.setTimezoneOverride', {
timezoneId: timezoneId || '',
});
}
catch (error) {
if (error.message.includes('Invalid timezone'))
throw new Error(`Invalid timezone ID: ${timezoneId}`);
throw error;
}
}
async emulateVisionDeficiency(type) {
const visionDeficiencies = new Set(Object.keys(VisionDeficiency));
try {
helper_1.assert(!type || visionDeficiencies.has(type), `Unsupported vision deficiency: ${type}`);
await this._client.send('Emulation.setEmulatedVisionDeficiency', {
type: type || 'none',
});
}
catch (error) {
throw error;
}
}
async setViewport(viewport) {
const needsReload = await this._emulationManager.emulateViewport(viewport);
this._viewport = viewport;
if (needsReload)
await this.reload();
}
viewport() {
return this._viewport;
}
async evaluate(pageFunction, ...args) {
return this._frameManager
.mainFrame()
.evaluate(pageFunction, ...args);
}
async evaluateOnNewDocument(pageFunction, ...args) {
const source = helper_1.helper.evaluationString(pageFunction, ...args);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source,
});
}
async setCacheEnabled(enabled = true) {
await this._frameManager.networkManager().setCacheEnabled(enabled);
}
async screenshot(options = {}) {
let screenshotType = null;
// options.type takes precedence over inferring the type from options.path
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
if (options.type) {
helper_1.assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
screenshotType = options.type;
}
else if (options.path) {
const mimeType = mime.getType(options.path);
if (mimeType === 'image/png')
screenshotType = 'png';
else if (mimeType === 'image/jpeg')
screenshotType = 'jpeg';
helper_1.assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType);
}
if (!screenshotType)
screenshotType = 'png';
if (options.quality) {
helper_1.assert(screenshotType === 'jpeg', 'options.quality is unsupported for the ' +
screenshotType +
' screenshots');
helper_1.assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' +
typeof options.quality);
helper_1.assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
helper_1.assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' +
options.quality);
}
helper_1.assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive');
if (options.clip) {
helper_1.assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' +
typeof options.clip.x);
helper_1.assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' +
typeof options.clip.y);
helper_1.assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' +
typeof options.clip.width);
helper_1.assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' +
typeof options.clip.height);
helper_1.assert(options.clip.width !== 0, 'Expected options.clip.width not to be 0.');
helper_1.assert(options.clip.height !== 0, 'Expected options.clip.height not to be 0.');
}
return this._screenshotTaskQueue.postTask(() => this._screenshotTask(screenshotType, options));
}
async _screenshotTask(format, options) {
await this._client.send('Target.activateTarget', {
targetId: this._target._targetId,
});
let clip = options.clip ? processClip(options.clip) : undefined;
if (options.fullPage) {
const metrics = await this._client.send('Page.getLayoutMetrics');
const width = Math.ceil(metrics.contentSize.width);
const height = Math.ceil(metrics.contentSize.height);
// Overwrite clip for full page at all times.
clip = { x: 0, y: 0, width, height, scale: 1 };
const { isMobile = false, deviceScaleFactor = 1, isLandscape = false } = this._viewport || {};
const screenOrientation = isLandscape
? { angle: 90, type: 'landscapePrimary' }
: { angle: 0, type: 'portraitPrimary' };
await this._client.send('Emulation.setDeviceMetricsOverride', {
mobile: isMobile,
width,
height,
deviceScaleFactor,
screenOrientation,
});
}
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
if (shouldSetDefaultBackground)
await this._client.send('Emulation.setDefaultBackgroundColorOverride', {
color: { r: 0, g: 0, b: 0, a: 0 },
});
const result = await this._client.send('Page.captureScreenshot', {
format,
quality: options.quality,
clip,
});
if (shouldSetDefaultBackground)
await this._client.send('Emulation.setDefaultBackgroundColorOverride');
if (options.fullPage && this._viewport)
await this.setViewport(this._viewport);
const buffer = options.encoding === 'base64'
? result.data
: Buffer.from(result.data, 'base64');
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;
function processClip(clip) {
const x = Math.round(clip.x);
const y = Math.round(clip.y);
const width = Math.round(clip.width + clip.x - x);
const height = Math.round(clip.height + clip.y - y);
return { x, y, width, height, scale: 1 };
}
}
async pdf(options = {}) {
const { scale = 1, displayHeaderFooter = false, headerTemplate = '', footerTemplate = '', printBackground = false, landscape = false, pageRanges = '', preferCSSPageSize = false, margin = {}, path = null, } = options;
let paperWidth = 8.5;
let paperHeight = 11;
if (options.format) {
const format = paperFormats[options.format.toLowerCase()];
helper_1.assert(format, 'Unknown paper format: ' + options.format);
paperWidth = format.width;
paperHeight = format.height;
}
else {
paperWidth = convertPrintParameterToInches(options.width) || paperWidth;
paperHeight =
convertPrintParameterToInches(options.height) || paperHeight;
}
const marginTop = convertPrintParameterToInches(margin.top) || 0;
const marginLeft = convertPrintParameterToInches(margin.left) || 0;
const marginBottom = convertPrintParameterToInches(margin.bottom) || 0;
const marginRight = convertPrintParameterToInches(margin.right) || 0;
const result = await this._client.send('Page.printToPDF', {
transferMode: 'ReturnAsStream',
landscape,
displayHeaderFooter,
headerTemplate,
footerTemplate,
printBackground,
scale,
paperWidth,
paperHeight,
marginTop,
marginBottom,
marginLeft,
marginRight,
pageRanges,
preferCSSPageSize,
});
return await helper_1.helper.readProtocolStream(this._client, result.stream, path);
}
async title() {
return this.mainFrame().title();
}
async close(options = { runBeforeUnload: undefined }) {
helper_1.assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
const runBeforeUnload = !!options.runBeforeUnload;
if (runBeforeUnload) {
await this._client.send('Page.close');
}
else {
await this._client._connection.send('Target.closeTarget', {
targetId: this._target._targetId,
});
await this._target._isClosedPromise;
}
}
isClosed() {
return this._closed;
}
get mouse() {
return this._mouse;
}
click(selector, options = {}) {
return this.mainFrame().click(selector, options);
}
focus(selector) {
return this.mainFrame().focus(selector);
}
hover(selector) {
return this.mainFrame().hover(selector);
}
select(selector, ...values) {
return this.mainFrame().select(selector, ...values);
}
tap(selector) {
return this.mainFrame().tap(selector);
}
type(selector, text, options) {
return this.mainFrame().type(selector, text, options);
}
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForSelector(selector, options = {}) {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath, options = {}) {
return this.mainFrame().waitForXPath(xpath, options);
}
waitForFunction(pageFunction, options = {}, ...args) {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
}
exports.Page = Page;
const supportedMetrics = new Set([
'Timestamp',
'Documents',
'Frames',
'JSEventListeners',
'Nodes',
'LayoutCount',
'RecalcStyleCount',
'LayoutDuration',
'RecalcStyleDuration',
'ScriptDuration',
'TaskDuration',
'JSHeapUsedSize',
'JSHeapTotalSize',
]);
const unitToPixels = {
px: 1,
in: 96,
cm: 37.8,
mm: 3.78,
};
function convertPrintParameterToInches(parameter) {
if (typeof parameter === 'undefined')
return undefined;
let pixels;
if (helper_1.helper.isNumber(parameter)) {
// Treat numbers as pixel values to be aligned with phantom's paperSize.
pixels = /** @type {number} */ parameter;
}
else if (helper_1.helper.isString(parameter)) {
const text = /** @type {string} */ parameter;
let unit = text.substring(text.length - 2).toLowerCase();
let valueText = '';
if (unitToPixels.hasOwnProperty(unit)) {
valueText = text.substring(0, text.length - 2);
}
else {
// In case of unknown unit try to parse the whole parameter as number of pixels.
// This is consistent with phantom's paperSize behavior.
unit = 'px';
valueText = text;
}
const value = Number(valueText);
helper_1.assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
pixels = value * unitToPixels[unit];
}
else {
throw new Error('page.pdf() Cannot handle parameter type: ' + typeof parameter);
}
return pixels / 96;
}
+64
View File
@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PipeTransport = void 0;
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const helper_1 = require("./helper");
class PipeTransport {
constructor(pipeWrite, pipeRead) {
this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [
helper_1.helper.addEventListener(pipeRead, 'data', (buffer) => this._dispatch(buffer)),
helper_1.helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose)
this.onclose.call(null);
}),
helper_1.helper.addEventListener(pipeRead, 'error', helper_1.debugError),
helper_1.helper.addEventListener(pipeWrite, 'error', helper_1.debugError),
];
this.onmessage = null;
this.onclose = null;
}
send(message) {
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
}
_dispatch(buffer) {
let end = buffer.indexOf('\0');
if (end === -1) {
this._pendingMessage += buffer.toString();
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage)
this.onmessage.call(null, message);
let start = end + 1;
end = buffer.indexOf('\0', start);
while (end !== -1) {
if (this.onmessage)
this.onmessage.call(null, buffer.toString(undefined, start, end));
start = end + 1;
end = buffer.indexOf('\0', start);
}
this._pendingMessage = buffer.toString(undefined, start);
}
close() {
this._pipeWrite = null;
helper_1.helper.removeEventListeners(this._eventListeners);
}
}
exports.PipeTransport = PipeTransport;
+106
View File
@@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Puppeteer = void 0;
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Launcher_1 = require("./Launcher");
const BrowserFetcher_1 = require("./BrowserFetcher");
const Errors_1 = require("./Errors");
const DeviceDescriptors_1 = require("./DeviceDescriptors");
const QueryHandler = require("./QueryHandler");
class Puppeteer {
constructor(projectRoot, preferredRevision, isPuppeteerCore, productName) {
this._changedProduct = false;
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
// track changes to Launcher configuration via options or environment variables
this.__productName = productName;
}
launch(options = {}) {
if (options.product)
this._productName = options.product;
return this._launcher.launch(options);
}
connect(options) {
if (options.product)
this._productName = options.product;
return this._launcher.connect(options);
}
set _productName(name) {
if (this.__productName !== name)
this._changedProduct = true;
this.__productName = name;
}
get _productName() {
return this.__productName;
}
executablePath() {
return this._launcher.executablePath();
}
get _launcher() {
if (!this._lazyLauncher ||
this._lazyLauncher.product !== this._productName ||
this._changedProduct) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
switch (this._productName) {
case 'firefox':
this._preferredRevision = packageJson.puppeteer.firefox_revision;
break;
case 'chrome':
default:
this._preferredRevision = packageJson.puppeteer.chromium_revision;
}
this._changedProduct = false;
this._lazyLauncher = Launcher_1.default(this._projectRoot, this._preferredRevision, this._isPuppeteerCore, this._productName);
}
return this._lazyLauncher;
}
get product() {
return this._launcher.product;
}
get devices() {
return DeviceDescriptors_1.devicesMap;
}
get errors() {
return Errors_1.puppeteerErrors;
}
defaultArgs(options) {
return this._launcher.defaultArgs(options);
}
createBrowserFetcher(options) {
return new BrowserFetcher_1.BrowserFetcher(this._projectRoot, options);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_registerCustomQueryHandler(name, queryHandler) {
QueryHandler.registerCustomQueryHandler(name, queryHandler);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_unregisterCustomQueryHandler(name) {
QueryHandler.unregisterCustomQueryHandler(name);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_customQueryHandlers() {
return QueryHandler.customQueryHandlers();
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_clearQueryHandlers() {
QueryHandler.clearQueryHandlers();
}
}
exports.Puppeteer = Puppeteer;
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+66
View File
@@ -0,0 +1,66 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getQueryHandlerAndSelector = exports.clearQueryHandlers = exports.customQueryHandlers = exports.unregisterCustomQueryHandler = exports.registerCustomQueryHandler = void 0;
const _customQueryHandlers = new Map();
function registerCustomQueryHandler(name, handler) {
if (_customQueryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`);
const isValidName = /^[a-zA-Z]+$/.test(name);
if (!isValidName)
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
_customQueryHandlers.set(name, handler);
}
exports.registerCustomQueryHandler = registerCustomQueryHandler;
/**
* @param {string} name
*/
function unregisterCustomQueryHandler(name) {
_customQueryHandlers.delete(name);
}
exports.unregisterCustomQueryHandler = unregisterCustomQueryHandler;
function customQueryHandlers() {
return _customQueryHandlers;
}
exports.customQueryHandlers = customQueryHandlers;
function clearQueryHandlers() {
_customQueryHandlers.clear();
}
exports.clearQueryHandlers = clearQueryHandlers;
function getQueryHandlerAndSelector(selector, defaultQueryHandler) {
const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector);
if (!hasCustomQueryHandler)
return { updatedSelector: selector, queryHandler: defaultQueryHandler };
const index = selector.indexOf('/');
const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1);
const queryHandler = customQueryHandlers().get(name);
if (!queryHandler)
throw new Error(`Query set to use "${name}", but no query handler of that name was found`);
return {
updatedSelector,
queryHandler,
};
}
exports.getQueryHandlerAndSelector = getQueryHandlerAndSelector;
module.exports = {
registerCustomQueryHandler,
unregisterCustomQueryHandler,
customQueryHandlers,
getQueryHandlerAndSelector,
clearQueryHandlers,
};
+47
View File
@@ -0,0 +1,47 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecurityDetails = void 0;
class SecurityDetails {
constructor(securityPayload) {
this._subjectName = securityPayload.subjectName;
this._issuer = securityPayload.issuer;
this._validFrom = securityPayload.validFrom;
this._validTo = securityPayload.validTo;
this._protocol = securityPayload.protocol;
this._sanList = securityPayload.sanList;
}
subjectName() {
return this._subjectName;
}
issuer() {
return this._issuer;
}
validFrom() {
return this._validFrom;
}
validTo() {
return this._validTo;
}
protocol() {
return this._protocol;
}
subjectAlternativeNames() {
return this._sanList;
}
}
exports.SecurityDetails = SecurityDetails;
+111
View File
@@ -0,0 +1,111 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Target = void 0;
const Events_1 = require("./Events");
const Page_1 = require("./Page");
const WebWorker_1 = require("./WebWorker");
class Target {
constructor(targetInfo, browserContext, sessionFactory, ignoreHTTPSErrors, defaultViewport) {
this._targetInfo = targetInfo;
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
this._sessionFactory = sessionFactory;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
/** @type {?Promise<!Puppeteer.Page>} */
this._pagePromise = null;
/** @type {?Promise<!WebWorker>} */
this._workerPromise = null;
this._initializedPromise = new Promise((fulfill) => (this._initializedCallback = fulfill)).then(async (success) => {
if (!success)
return false;
const opener = this.opener();
if (!opener || !opener._pagePromise || this.type() !== 'page')
return true;
const openerPage = await opener._pagePromise;
if (!openerPage.listenerCount(Events_1.Events.Page.Popup))
return true;
const popupPage = await this.page();
openerPage.emit(Events_1.Events.Page.Popup, popupPage);
return true;
});
this._isClosedPromise = new Promise((fulfill) => (this._closedCallback = fulfill));
this._isInitialized =
this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized)
this._initializedCallback(true);
}
createCDPSession() {
return this._sessionFactory();
}
async page() {
if ((this._targetInfo.type === 'page' ||
this._targetInfo.type === 'background_page' ||
this._targetInfo.type === 'webview') &&
!this._pagePromise) {
this._pagePromise = this._sessionFactory().then((client) => Page_1.Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport));
}
return this._pagePromise;
}
async worker() {
if (this._targetInfo.type !== 'service_worker' &&
this._targetInfo.type !== 'shared_worker')
return null;
if (!this._workerPromise) {
// TODO(einbinder): Make workers send their console logs.
this._workerPromise = this._sessionFactory().then((client) => new WebWorker_1.WebWorker(client, this._targetInfo.url, () => { } /* consoleAPICalled */, () => { } /* exceptionThrown */));
}
return this._workerPromise;
}
url() {
return this._targetInfo.url;
}
type() {
const type = this._targetInfo.type;
if (type === 'page' ||
type === 'background_page' ||
type === 'service_worker' ||
type === 'shared_worker' ||
type === 'browser' ||
type === 'webview')
return type;
return 'other';
}
browser() {
return this._browserContext.browser();
}
browserContext() {
return this._browserContext;
}
opener() {
const { openerId } = this._targetInfo;
if (!openerId)
return null;
return this.browser()._targets.get(openerId);
}
_targetInfoChanged(targetInfo) {
this._targetInfo = targetInfo;
if (!this._isInitialized &&
(this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) {
this._isInitialized = true;
this._initializedCallback(true);
return;
}
}
}
exports.Target = Target;
+47
View File
@@ -0,0 +1,47 @@
"use strict";
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimeoutSettings = void 0;
const DEFAULT_TIMEOUT = 30000;
class TimeoutSettings {
constructor() {
this._defaultTimeout = null;
this._defaultNavigationTimeout = null;
}
setDefaultTimeout(timeout) {
this._defaultTimeout = timeout;
}
/**
* @param {number} timeout
*/
setDefaultNavigationTimeout(timeout) {
this._defaultNavigationTimeout = timeout;
}
navigationTimeout() {
if (this._defaultNavigationTimeout !== null)
return this._defaultNavigationTimeout;
if (this._defaultTimeout !== null)
return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
timeout() {
if (this._defaultTimeout !== null)
return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
}
exports.TimeoutSettings = TimeoutSettings;
+65
View File
@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tracing = void 0;
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const helper_1 = require("./helper");
class Tracing {
constructor(client) {
this._recording = false;
this._path = '';
this._client = client;
}
async start(options = {}) {
helper_1.assert(!this._recording, 'Cannot start recording trace while already recording trace.');
const defaultCategories = [
'-*',
'devtools.timeline',
'v8.execute',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame',
'toplevel',
'blink.console',
'blink.user_timing',
'latencyInfo',
'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-v8.cpu_profiler.hires',
];
const { path = null, screenshots = false, categories = defaultCategories, } = options;
if (screenshots)
categories.push('disabled-by-default-devtools.screenshot');
this._path = path;
this._recording = true;
await this._client.send('Tracing.start', {
transferMode: 'ReturnAsStream',
categories: categories.join(','),
});
}
async stop() {
let fulfill;
const contentPromise = new Promise((x) => (fulfill = x));
this._client.once('Tracing.tracingComplete', (event) => {
helper_1.helper
.readProtocolStream(this._client, event.stream, this._path)
.then(fulfill);
});
await this._client.send('Tracing.end');
this._recording = false;
return contentPromise;
}
}
exports.Tracing = Tracing;
+403
View File
@@ -0,0 +1,403 @@
"use strict";
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.keyDefinitions = void 0;
exports.keyDefinitions = {
'0': { keyCode: 48, key: '0', code: 'Digit0' },
'1': { keyCode: 49, key: '1', code: 'Digit1' },
'2': { keyCode: 50, key: '2', code: 'Digit2' },
'3': { keyCode: 51, key: '3', code: 'Digit3' },
'4': { keyCode: 52, key: '4', code: 'Digit4' },
'5': { keyCode: 53, key: '5', code: 'Digit5' },
'6': { keyCode: 54, key: '6', code: 'Digit6' },
'7': { keyCode: 55, key: '7', code: 'Digit7' },
'8': { keyCode: 56, key: '8', code: 'Digit8' },
'9': { keyCode: 57, key: '9', code: 'Digit9' },
Power: { key: 'Power', code: 'Power' },
Eject: { key: 'Eject', code: 'Eject' },
Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' },
Help: { keyCode: 6, code: 'Help', key: 'Help' },
Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },
Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },
Numpad5: {
keyCode: 12,
shiftKeyCode: 101,
key: 'Clear',
code: 'Numpad5',
shiftKey: '5',
location: 3,
},
NumpadEnter: {
keyCode: 13,
code: 'NumpadEnter',
key: 'Enter',
text: '\r',
location: 3,
},
Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 },
ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 },
ControlLeft: {
keyCode: 17,
code: 'ControlLeft',
key: 'Control',
location: 1,
},
ControlRight: {
keyCode: 17,
code: 'ControlRight',
key: 'Control',
location: 2,
},
AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 },
AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 },
Pause: { keyCode: 19, code: 'Pause', key: 'Pause' },
CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },
Convert: { keyCode: 28, code: 'Convert', key: 'Convert' },
NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' },
Space: { keyCode: 32, code: 'Space', key: ' ' },
Numpad9: {
keyCode: 33,
shiftKeyCode: 105,
key: 'PageUp',
code: 'Numpad9',
shiftKey: '9',
location: 3,
},
PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' },
Numpad3: {
keyCode: 34,
shiftKeyCode: 99,
key: 'PageDown',
code: 'Numpad3',
shiftKey: '3',
location: 3,
},
PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' },
End: { keyCode: 35, code: 'End', key: 'End' },
Numpad1: {
keyCode: 35,
shiftKeyCode: 97,
key: 'End',
code: 'Numpad1',
shiftKey: '1',
location: 3,
},
Home: { keyCode: 36, code: 'Home', key: 'Home' },
Numpad7: {
keyCode: 36,
shiftKeyCode: 103,
key: 'Home',
code: 'Numpad7',
shiftKey: '7',
location: 3,
},
ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
Numpad4: {
keyCode: 37,
shiftKeyCode: 100,
key: 'ArrowLeft',
code: 'Numpad4',
shiftKey: '4',
location: 3,
},
Numpad8: {
keyCode: 38,
shiftKeyCode: 104,
key: 'ArrowUp',
code: 'Numpad8',
shiftKey: '8',
location: 3,
},
ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
Numpad6: {
keyCode: 39,
shiftKeyCode: 102,
key: 'ArrowRight',
code: 'Numpad6',
shiftKey: '6',
location: 3,
},
Numpad2: {
keyCode: 40,
shiftKeyCode: 98,
key: 'ArrowDown',
code: 'Numpad2',
shiftKey: '2',
location: 3,
},
ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
Select: { keyCode: 41, code: 'Select', key: 'Select' },
Open: { keyCode: 43, code: 'Open', key: 'Execute' },
PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
Insert: { keyCode: 45, code: 'Insert', key: 'Insert' },
Numpad0: {
keyCode: 45,
shiftKeyCode: 96,
key: 'Insert',
code: 'Numpad0',
shiftKey: '0',
location: 3,
},
Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },
NumpadDecimal: {
keyCode: 46,
shiftKeyCode: 110,
code: 'NumpadDecimal',
key: '\u0000',
shiftKey: '.',
location: 3,
},
Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' },
Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' },
Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' },
Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' },
Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' },
Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' },
Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' },
Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' },
Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' },
Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' },
KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' },
KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' },
KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' },
KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' },
KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' },
KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' },
KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' },
KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' },
KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' },
KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' },
KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' },
KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' },
KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' },
KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' },
KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' },
KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' },
KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' },
KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' },
KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' },
KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' },
KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' },
KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' },
KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' },
KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' },
KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' },
KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' },
MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 },
MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 },
ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
NumpadMultiply: {
keyCode: 106,
code: 'NumpadMultiply',
key: '*',
location: 3,
},
NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 },
NumpadSubtract: {
keyCode: 109,
code: 'NumpadSubtract',
key: '-',
location: 3,
},
NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 },
F1: { keyCode: 112, code: 'F1', key: 'F1' },
F2: { keyCode: 113, code: 'F2', key: 'F2' },
F3: { keyCode: 114, code: 'F3', key: 'F3' },
F4: { keyCode: 115, code: 'F4', key: 'F4' },
F5: { keyCode: 116, code: 'F5', key: 'F5' },
F6: { keyCode: 117, code: 'F6', key: 'F6' },
F7: { keyCode: 118, code: 'F7', key: 'F7' },
F8: { keyCode: 119, code: 'F8', key: 'F8' },
F9: { keyCode: 120, code: 'F9', key: 'F9' },
F10: { keyCode: 121, code: 'F10', key: 'F10' },
F11: { keyCode: 122, code: 'F11', key: 'F11' },
F12: { keyCode: 123, code: 'F12', key: 'F12' },
F13: { keyCode: 124, code: 'F13', key: 'F13' },
F14: { keyCode: 125, code: 'F14', key: 'F14' },
F15: { keyCode: 126, code: 'F15', key: 'F15' },
F16: { keyCode: 127, code: 'F16', key: 'F16' },
F17: { keyCode: 128, code: 'F17', key: 'F17' },
F18: { keyCode: 129, code: 'F18', key: 'F18' },
F19: { keyCode: 130, code: 'F19', key: 'F19' },
F20: { keyCode: 131, code: 'F20', key: 'F20' },
F21: { keyCode: 132, code: 'F21', key: 'F21' },
F22: { keyCode: 133, code: 'F22', key: 'F22' },
F23: { keyCode: 134, code: 'F23', key: 'F23' },
F24: { keyCode: 135, code: 'F24', key: 'F24' },
NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' },
ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
AudioVolumeMute: {
keyCode: 173,
code: 'AudioVolumeMute',
key: 'AudioVolumeMute',
},
AudioVolumeDown: {
keyCode: 174,
code: 'AudioVolumeDown',
key: 'AudioVolumeDown',
},
AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' },
MediaTrackNext: {
keyCode: 176,
code: 'MediaTrackNext',
key: 'MediaTrackNext',
},
MediaTrackPrevious: {
keyCode: 177,
code: 'MediaTrackPrevious',
key: 'MediaTrackPrevious',
},
MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' },
MediaPlayPause: {
keyCode: 179,
code: 'MediaPlayPause',
key: 'MediaPlayPause',
},
Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' },
Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' },
NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 },
Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' },
Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' },
Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' },
Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' },
Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' },
BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' },
Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' },
BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' },
Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: "'" },
AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' },
Props: { keyCode: 247, code: 'Props', key: 'CrSel' },
Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' },
Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 },
Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 },
Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 },
Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 },
Accept: { keyCode: 30, key: 'Accept' },
ModeChange: { keyCode: 31, key: 'ModeChange' },
' ': { keyCode: 32, key: ' ', code: 'Space' },
Print: { keyCode: 42, key: 'Print' },
Execute: { keyCode: 43, key: 'Execute', code: 'Open' },
'\u0000': { keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3 },
a: { keyCode: 65, key: 'a', code: 'KeyA' },
b: { keyCode: 66, key: 'b', code: 'KeyB' },
c: { keyCode: 67, key: 'c', code: 'KeyC' },
d: { keyCode: 68, key: 'd', code: 'KeyD' },
e: { keyCode: 69, key: 'e', code: 'KeyE' },
f: { keyCode: 70, key: 'f', code: 'KeyF' },
g: { keyCode: 71, key: 'g', code: 'KeyG' },
h: { keyCode: 72, key: 'h', code: 'KeyH' },
i: { keyCode: 73, key: 'i', code: 'KeyI' },
j: { keyCode: 74, key: 'j', code: 'KeyJ' },
k: { keyCode: 75, key: 'k', code: 'KeyK' },
l: { keyCode: 76, key: 'l', code: 'KeyL' },
m: { keyCode: 77, key: 'm', code: 'KeyM' },
n: { keyCode: 78, key: 'n', code: 'KeyN' },
o: { keyCode: 79, key: 'o', code: 'KeyO' },
p: { keyCode: 80, key: 'p', code: 'KeyP' },
q: { keyCode: 81, key: 'q', code: 'KeyQ' },
r: { keyCode: 82, key: 'r', code: 'KeyR' },
s: { keyCode: 83, key: 's', code: 'KeyS' },
t: { keyCode: 84, key: 't', code: 'KeyT' },
u: { keyCode: 85, key: 'u', code: 'KeyU' },
v: { keyCode: 86, key: 'v', code: 'KeyV' },
w: { keyCode: 87, key: 'w', code: 'KeyW' },
x: { keyCode: 88, key: 'x', code: 'KeyX' },
y: { keyCode: 89, key: 'y', code: 'KeyY' },
z: { keyCode: 90, key: 'z', code: 'KeyZ' },
Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 },
'*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 },
'+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 },
'-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 },
'/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 },
';': { keyCode: 186, key: ';', code: 'Semicolon' },
'=': { keyCode: 187, key: '=', code: 'Equal' },
',': { keyCode: 188, key: ',', code: 'Comma' },
'.': { keyCode: 190, key: '.', code: 'Period' },
'`': { keyCode: 192, key: '`', code: 'Backquote' },
'[': { keyCode: 219, key: '[', code: 'BracketLeft' },
'\\': { keyCode: 220, key: '\\', code: 'Backslash' },
']': { keyCode: 221, key: ']', code: 'BracketRight' },
"'": { keyCode: 222, key: "'", code: 'Quote' },
Attn: { keyCode: 246, key: 'Attn' },
CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' },
ExSel: { keyCode: 248, key: 'ExSel' },
EraseEof: { keyCode: 249, key: 'EraseEof' },
Play: { keyCode: 250, key: 'Play' },
ZoomOut: { keyCode: 251, key: 'ZoomOut' },
')': { keyCode: 48, key: ')', code: 'Digit0' },
'!': { keyCode: 49, key: '!', code: 'Digit1' },
'@': { keyCode: 50, key: '@', code: 'Digit2' },
'#': { keyCode: 51, key: '#', code: 'Digit3' },
$: { keyCode: 52, key: '$', code: 'Digit4' },
'%': { keyCode: 53, key: '%', code: 'Digit5' },
'^': { keyCode: 54, key: '^', code: 'Digit6' },
'&': { keyCode: 55, key: '&', code: 'Digit7' },
'(': { keyCode: 57, key: '(', code: 'Digit9' },
A: { keyCode: 65, key: 'A', code: 'KeyA' },
B: { keyCode: 66, key: 'B', code: 'KeyB' },
C: { keyCode: 67, key: 'C', code: 'KeyC' },
D: { keyCode: 68, key: 'D', code: 'KeyD' },
E: { keyCode: 69, key: 'E', code: 'KeyE' },
F: { keyCode: 70, key: 'F', code: 'KeyF' },
G: { keyCode: 71, key: 'G', code: 'KeyG' },
H: { keyCode: 72, key: 'H', code: 'KeyH' },
I: { keyCode: 73, key: 'I', code: 'KeyI' },
J: { keyCode: 74, key: 'J', code: 'KeyJ' },
K: { keyCode: 75, key: 'K', code: 'KeyK' },
L: { keyCode: 76, key: 'L', code: 'KeyL' },
M: { keyCode: 77, key: 'M', code: 'KeyM' },
N: { keyCode: 78, key: 'N', code: 'KeyN' },
O: { keyCode: 79, key: 'O', code: 'KeyO' },
P: { keyCode: 80, key: 'P', code: 'KeyP' },
Q: { keyCode: 81, key: 'Q', code: 'KeyQ' },
R: { keyCode: 82, key: 'R', code: 'KeyR' },
S: { keyCode: 83, key: 'S', code: 'KeyS' },
T: { keyCode: 84, key: 'T', code: 'KeyT' },
U: { keyCode: 85, key: 'U', code: 'KeyU' },
V: { keyCode: 86, key: 'V', code: 'KeyV' },
W: { keyCode: 87, key: 'W', code: 'KeyW' },
X: { keyCode: 88, key: 'X', code: 'KeyX' },
Y: { keyCode: 89, key: 'Y', code: 'KeyY' },
Z: { keyCode: 90, key: 'Z', code: 'KeyZ' },
':': { keyCode: 186, key: ':', code: 'Semicolon' },
'<': { keyCode: 188, key: '<', code: 'Comma' },
_: { keyCode: 189, key: '_', code: 'Minus' },
'>': { keyCode: 190, key: '>', code: 'Period' },
'?': { keyCode: 191, key: '?', code: 'Slash' },
'~': { keyCode: 192, key: '~', code: 'Backquote' },
'{': { keyCode: 219, key: '{', code: 'BracketLeft' },
'|': { keyCode: 220, key: '|', code: 'Backslash' },
'}': { keyCode: 221, key: '}', code: 'BracketRight' },
'"': { keyCode: 222, key: '"', code: 'Quote' },
SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 },
SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 },
Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 },
Call: { key: 'Call', code: 'Call', location: 4 },
EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 },
VolumeDown: {
keyCode: 182,
key: 'VolumeDown',
code: 'VolumeDown',
location: 4,
},
VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 },
};
+53
View File
@@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketTransport = void 0;
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const NodeWebSocket = require("ws");
class WebSocketTransport {
constructor(ws) {
this._ws = ws;
this._ws.addEventListener('message', (event) => {
if (this.onmessage)
this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', () => {
if (this.onclose)
this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => { });
this.onmessage = null;
this.onclose = null;
}
static create(url) {
return new Promise((resolve, reject) => {
const ws = new NodeWebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024,
});
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
ws.addEventListener('error', reject);
});
}
send(message) {
this._ws.send(message);
}
close() {
this._ws.close();
}
}
exports.WebSocketTransport = WebSocketTransport;
+54
View File
@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebWorker = void 0;
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const events_1 = require("events");
const helper_1 = require("./helper");
const ExecutionContext_1 = require("./ExecutionContext");
const JSHandle_1 = require("./JSHandle");
class WebWorker extends events_1.EventEmitter {
constructor(client, url, consoleAPICalled, exceptionThrown) {
super();
this._client = client;
this._url = url;
this._executionContextPromise = new Promise((x) => (this._executionContextCallback = x));
let jsHandleFactory;
this._client.once('Runtime.executionContextCreated', async (event) => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
jsHandleFactory = (remoteObject) => new JSHandle_1.JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext_1.ExecutionContext(client, event.context, null);
this._executionContextCallback(executionContext);
});
// This might fail if the target is closed before we recieve all execution contexts.
this._client.send('Runtime.enable', {}).catch(helper_1.debugError);
this._client.on('Runtime.consoleAPICalled', (event) => consoleAPICalled(event.type, event.args.map(jsHandleFactory), event.stackTrace));
this._client.on('Runtime.exceptionThrown', (exception) => exceptionThrown(exception.exceptionDetails));
}
url() {
return this._url;
}
async executionContext() {
return this._executionContextPromise;
}
async evaluate(pageFunction, ...args) {
return (await this._executionContextPromise).evaluate(pageFunction, ...args);
}
async evaluateHandle(pageFunction, ...args) {
return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args);
}
}
exports.WebWorker = WebWorker;
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* This file is used in two places:
* 1) the coverage-utils use it to gain a list of all methods we check for test coverage on
* 2) index.js uses it to iterate through all methods and call helper.installAsyncStackHooks on
*/
module.exports = {
Accessibility: require('./Accessibility').Accessibility,
Browser: require('./Browser').Browser,
BrowserContext: require('./Browser').BrowserContext,
BrowserFetcher: require('./BrowserFetcher').BrowserFetcher,
CDPSession: require('./Connection').CDPSession,
ConsoleMessage: require('./ConsoleMessage').ConsoleMessage,
Coverage: require('./Coverage').Coverage,
Dialog: require('./Dialog').Dialog,
ElementHandle: require('./JSHandle').ElementHandle,
ExecutionContext: require('./ExecutionContext').ExecutionContext,
FileChooser: require('./FileChooser').FileChooser,
Frame: require('./FrameManager').Frame,
JSHandle: require('./JSHandle').JSHandle,
Keyboard: require('./Input').Keyboard,
Mouse: require('./Input').Mouse,
Page: require('./Page').Page,
Puppeteer: require('./Puppeteer').Puppeteer,
HTTPRequest: require('./HTTPRequest').HTTPRequest,
HTTPResponse: require('./HTTPResponse').HTTPResponse,
SecurityDetails: require('./SecurityDetails').SecurityDetails,
Target: require('./Target').Target,
TimeoutError: require('./Errors').TimeoutError,
Touchscreen: require('./Input').Touchscreen,
Tracing: require('./Tracing').Tracing,
WebWorker: require('./WebWorker').WebWorker,
};
+219
View File
@@ -0,0 +1,219 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.helper = exports.assert = exports.debugError = void 0;
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Errors_1 = require("./Errors");
const debug = require("debug");
const fs = require("fs");
const util_1 = require("util");
const openAsync = util_1.promisify(fs.open);
const writeAsync = util_1.promisify(fs.write);
const closeAsync = util_1.promisify(fs.close);
exports.debugError = debug('puppeteer:error');
function assert(value, message) {
if (!value)
throw new Error(message);
}
exports.assert = assert;
function getExceptionMessage(exceptionDetails) {
if (exceptionDetails.exception)
return (exceptionDetails.exception.description || exceptionDetails.exception.value);
let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) {
const location = callframe.url +
':' +
callframe.lineNumber +
':' +
callframe.columnNumber;
const functionName = callframe.functionName || '<anonymous>';
message += `\n at ${functionName} (${location})`;
}
}
return message;
}
function valueFromRemoteObject(remoteObject) {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
return BigInt(remoteObject.unserializableValue.replace('n', ''));
switch (remoteObject.unserializableValue) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error('Unsupported unserializable value: ' +
remoteObject.unserializableValue);
}
}
return remoteObject.value;
}
async function releaseObject(client, remoteObject) {
if (!remoteObject.objectId)
return;
await client
.send('Runtime.releaseObject', { objectId: remoteObject.objectId })
.catch((error) => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
exports.debugError(error);
});
}
function installAsyncStackHooks(classType) {
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' ||
typeof methodName !== 'string' ||
methodName.startsWith('_') ||
typeof method !== 'function' ||
method.constructor.name !== 'AsyncFunction')
continue;
Reflect.set(classType.prototype, methodName, function (...args) {
const syncStack = {
stack: '',
};
Error.captureStackTrace(syncStack);
return method.call(this, ...args).catch((error) => {
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
const clientStack = stack.substring(stack.indexOf('\n'));
if (error instanceof Error &&
error.stack &&
!error.stack.includes(clientStack))
error.stack += '\n -- ASYNC --\n' + stack;
throw error;
});
});
}
}
function addEventListener(emitter, eventName, handler) {
emitter.on(eventName, handler);
return { emitter, eventName, handler };
}
function removeEventListeners(listeners) {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.length = 0;
}
function isString(obj) {
return typeof obj === 'string' || obj instanceof String;
}
function isNumber(obj) {
return typeof obj === 'number' || obj instanceof Number;
}
async function waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
let eventTimeout, resolveCallback, rejectCallback;
const promise = new Promise((resolve, reject) => {
resolveCallback = resolve;
rejectCallback = reject;
});
const listener = addEventListener(emitter, eventName, (event) => {
if (!predicate(event))
return;
resolveCallback(event);
});
if (timeout) {
eventTimeout = setTimeout(() => {
rejectCallback(new Errors_1.TimeoutError('Timeout exceeded while waiting for event'));
}, timeout);
}
function cleanup() {
removeEventListeners([listener]);
clearTimeout(eventTimeout);
}
const result = await Promise.race([promise, abortPromise]).then((r) => {
cleanup();
return r;
}, (error) => {
cleanup();
throw error;
});
if (result instanceof Error)
throw result;
return result;
}
function evaluationString(fun, ...args) {
if (isString(fun)) {
assert(args.length === 0, 'Cannot evaluate a string with arguments');
return fun;
}
function serializeArgument(arg) {
if (Object.is(arg, undefined))
return 'undefined';
return JSON.stringify(arg);
}
return `(${fun})(${args.map(serializeArgument).join(',')})`;
}
async function waitWithTimeout(promise, taskName, timeout) {
let reject;
const timeoutError = new Errors_1.TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
const timeoutPromise = new Promise((resolve, x) => (reject = x));
let timeoutTimer = null;
if (timeout)
timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
}
finally {
if (timeoutTimer)
clearTimeout(timeoutTimer);
}
}
async function readProtocolStream(client, handle, path) {
let eof = false;
let file;
if (path)
file = await openAsync(path, 'w');
const bufs = [];
while (!eof) {
const response = await client.send('IO.read', { handle });
eof = response.eof;
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
bufs.push(buf);
if (path)
await writeAsync(file, buf);
}
if (path)
await closeAsync(file);
await client.send('IO.close', { handle });
let resultBuffer = null;
try {
resultBuffer = Buffer.concat(bufs);
}
finally {
return resultBuffer;
}
}
exports.helper = {
promisify: util_1.promisify,
evaluationString,
readProtocolStream,
waitWithTimeout,
waitForEvent,
isString,
isNumber,
addEventListener,
removeEventListeners,
valueFromRemoteObject,
installAsyncStackHooks,
getExceptionMessage,
releaseObject,
};
+186
View File
@@ -0,0 +1,186 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserRunner = void 0;
const debug = require("debug");
const removeFolder = require("rimraf");
const childProcess = require("child_process");
const helper_1 = require("../helper");
const Connection_1 = require("../Connection");
const WebSocketTransport_1 = require("../WebSocketTransport");
const PipeTransport_1 = require("../PipeTransport");
const readline = require("readline");
const Errors_1 = require("../Errors");
const removeFolderAsync = helper_1.helper.promisify(removeFolder);
const debugLauncher = debug('puppeteer:launcher');
class BrowserRunner {
constructor(executablePath, processArguments, tempDirectory) {
this.proc = null;
this.connection = null;
this._closed = true;
this._listeners = [];
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
}
start(options = {}) {
const { handleSIGINT, handleSIGTERM, handleSIGHUP, dumpio, env, pipe, } = options;
let stdio = ['pipe', 'pipe', 'pipe'];
if (pipe) {
if (dumpio)
stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else
stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
helper_1.assert(!this.proc, 'This process has previously been started.');
debugLauncher(`Calling ${this._executablePath} ${this._processArguments.join(' ')}`);
this.proc = childProcess.spawn(this._executablePath, this._processArguments, {
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio,
});
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill) => {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch((error) => console.error(error));
}
else {
fulfill();
}
});
});
this._listeners = [
helper_1.helper.addEventListener(process, 'exit', this.kill.bind(this)),
];
if (handleSIGINT)
this._listeners.push(helper_1.helper.addEventListener(process, 'SIGINT', () => {
this.kill();
process.exit(130);
}));
if (handleSIGTERM)
this._listeners.push(helper_1.helper.addEventListener(process, 'SIGTERM', this.close.bind(this)));
if (handleSIGHUP)
this._listeners.push(helper_1.helper.addEventListener(process, 'SIGHUP', this.close.bind(this)));
}
close() {
if (this._closed)
return Promise.resolve();
helper_1.helper.removeEventListeners(this._listeners);
if (this._tempDirectory) {
this.kill();
}
else if (this.connection) {
// Attempt to close the browser gracefully
this.connection.send('Browser.close').catch((error) => {
helper_1.debugError(error);
this.kill();
});
}
return this._processClosing;
}
kill() {
helper_1.helper.removeEventListeners(this._listeners);
if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
else
process.kill(-this.proc.pid, 'SIGKILL');
}
catch (error) {
// the process might have already stopped
}
}
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(this._tempDirectory);
}
catch (error) { }
}
async setupConnection(options) {
const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(this.proc, timeout, preferredRevision);
const transport = await WebSocketTransport_1.WebSocketTransport.create(browserWSEndpoint);
this.connection = new Connection_1.Connection(browserWSEndpoint, transport, slowMo);
}
else {
// stdio was assigned during start(), and the 'pipe' option there adds the 4th and 5th items to stdio array
const { 3: pipeWrite, 4: pipeRead } = this.proc.stdio;
const transport = new PipeTransport_1.PipeTransport(pipeWrite, pipeRead);
this.connection = new Connection_1.Connection('', transport, slowMo);
}
return this.connection;
}
}
exports.BrowserRunner = BrowserRunner;
function waitForWSEndpoint(browserProcess, timeout, preferredRevision) {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [
helper_1.helper.addEventListener(rl, 'line', onLine),
helper_1.helper.addEventListener(rl, 'close', () => onClose()),
helper_1.helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper_1.helper.addEventListener(browserProcess, 'error', (error) => onClose(error)),
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
/**
* @param {!Error=} error
*/
function onClose(error) {
cleanup();
reject(new Error([
'Failed to launch the browser process!' +
(error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')));
}
function onTimeout() {
cleanup();
reject(new Errors_1.TimeoutError(`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`));
}
function onLine(line) {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match)
return;
cleanup();
resolve(match[1]);
}
function cleanup() {
if (timeoutId)
clearTimeout(timeoutId);
helper_1.helper.removeEventListeners(listeners);
}
});
}
+17
View File
@@ -0,0 +1,17 @@
"use strict";
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+15503
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
else
exec node "$basedir/../mime/cli.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mime/cli.js" $args
} else {
& "node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
else
exec node "$basedir/../rimraf/bin.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rimraf\bin.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+296
View File
@@ -0,0 +1,296 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.6.0](https://github.com/broofa/mime/compare/v2.5.2...v2.6.0) (2021-11-02)
### Features
* mime-db@1.50.0 ([cef0cc4](https://github.com/broofa/mime/commit/cef0cc484ff6d05ff1e12b54ca3e8b856fbc14d8))
### [2.5.2](https://github.com/broofa/mime/compare/v2.5.0...v2.5.2) (2021-02-17)
### Bug Fixes
* update to mime-db@1.46.0, fixes [#253](https://github.com/broofa/mime/issues/253) ([f10e6aa](https://github.com/broofa/mime/commit/f10e6aa62e1356de7e2491d7fb4374c8dac65800))
## [2.5.0](https://github.com/broofa/mime/compare/v2.4.7...v2.5.0) (2021-01-16)
### Features
* improved CLI ([#244](https://github.com/broofa/mime/issues/244)) ([c8a8356](https://github.com/broofa/mime/commit/c8a8356e3b27f3ef46b64b89b428fdb547b14d5f))
### [2.4.7](https://github.com/broofa/mime/compare/v2.4.6...v2.4.7) (2020-12-16)
### Bug Fixes
* update to latest mime-db ([43b09ef](https://github.com/broofa/mime/commit/43b09eff0233eacc449af2b1f99a19ba9e104a44))
### [2.4.6](https://github.com/broofa/mime/compare/v2.4.5...v2.4.6) (2020-05-27)
### Bug Fixes
* add cli.js to package.json files ([#237](https://github.com/broofa/mime/issues/237)) ([6c070bc](https://github.com/broofa/mime/commit/6c070bc298fa12a48e2ed126fbb9de641a1e7ebc))
### [2.4.5](https://github.com/broofa/mime/compare/v2.4.4...v2.4.5) (2020-05-01)
### Bug Fixes
* fix [#236](https://github.com/broofa/mime/issues/236) ([7f4ecd0](https://github.com/broofa/mime/commit/7f4ecd0d850ed22c9e3bfda2c11fc74e4dde12a7))
* update to latest mime-db ([c5cb3f2](https://github.com/broofa/mime/commit/c5cb3f2ab8b07642a066efbde1142af1b90c927b))
### [2.4.4](https://github.com/broofa/mime/compare/v2.4.3...v2.4.4) (2019-06-07)
### [2.4.3](https://github.com/broofa/mime/compare/v2.4.2...v2.4.3) (2019-05-15)
### [2.4.2](https://github.com/broofa/mime/compare/v2.4.1...v2.4.2) (2019-04-07)
### Bug Fixes
* don't use arrow function introduced in 2.4.1 ([2e00b5c](https://github.com/broofa/mime/commit/2e00b5c))
### [2.4.1](https://github.com/broofa/mime/compare/v2.4.0...v2.4.1) (2019-04-03)
### Bug Fixes
* update MDN and mime-db types ([3e567a9](https://github.com/broofa/mime/commit/3e567a9))
# [2.4.0](https://github.com/broofa/mime/compare/v2.3.1...v2.4.0) (2018-11-26)
### Features
* Bind exported methods ([9d2a7b8](https://github.com/broofa/mime/commit/9d2a7b8))
* update to mime-db@1.37.0 ([49e6e41](https://github.com/broofa/mime/commit/49e6e41))
### [2.3.1](https://github.com/broofa/mime/compare/v2.3.0...v2.3.1) (2018-04-11)
### Bug Fixes
* fix [#198](https://github.com/broofa/mime/issues/198) ([25ca180](https://github.com/broofa/mime/commit/25ca180))
# [2.3.0](https://github.com/broofa/mime/compare/v2.2.2...v2.3.0) (2018-04-11)
### Bug Fixes
* fix [#192](https://github.com/broofa/mime/issues/192) ([5c35df6](https://github.com/broofa/mime/commit/5c35df6))
### Features
* add travis-ci testing ([d64160f](https://github.com/broofa/mime/commit/d64160f))
### [2.2.2](https://github.com/broofa/mime/compare/v2.2.1...v2.2.2) (2018-03-30)
### Bug Fixes
* update types files to mime-db@1.32.0 ([85aac16](https://github.com/broofa/mime/commit/85aac16))
### [2.2.1](https://github.com/broofa/mime/compare/v2.2.0...v2.2.1) (2018-03-30)
### Bug Fixes
* Retain type->extension mappings for non-default types. Fixes [#180](https://github.com/broofa/mime/issues/180) ([b5c83fb](https://github.com/broofa/mime/commit/b5c83fb))
# [2.2.0](https://github.com/broofa/mime/compare/v2.1.0...v2.2.0) (2018-01-04)
### Features
* Retain type->extension mappings for non-default types. Fixes [#180](https://github.com/broofa/mime/issues/180) ([10f82ac](https://github.com/broofa/mime/commit/10f82ac))
# [2.1.0](https://github.com/broofa/mime/compare/v2.0.5...v2.1.0) (2017-12-22)
### Features
* Upgrade to mime-db@1.32.0. Fixes [#185](https://github.com/broofa/mime/issues/185) ([3f775ba](https://github.com/broofa/mime/commit/3f775ba))
### [2.0.5](https://github.com/broofa/mime/compare/v2.0.1...v2.0.5) (2017-12-22)
### Bug Fixes
* ES5 support (back to node v0.4) ([f14ccb6](https://github.com/broofa/mime/commit/f14ccb6))
# Changelog
### v2.0.4 (24/11/2017)
- [**closed**] Switch to mime-score module for resolving extension contention issues. [#182](https://github.com/broofa/mime/issues/182)
- [**closed**] Update mime-db to 1.31.0 in v1.x branch [#181](https://github.com/broofa/mime/issues/181)
---
## v1.5.0 (22/11/2017)
- [**closed**] need ES5 version ready in npm package [#179](https://github.com/broofa/mime/issues/179)
- [**closed**] mime-db no trace of iWork - pages / numbers / etc. [#178](https://github.com/broofa/mime/issues/178)
- [**closed**] How it works in brownser ? [#176](https://github.com/broofa/mime/issues/176)
- [**closed**] Missing `./Mime` [#175](https://github.com/broofa/mime/issues/175)
- [**closed**] Vulnerable Regular Expression [#167](https://github.com/broofa/mime/issues/167)
---
### v2.0.3 (25/09/2017)
*No changelog for this release.*
---
### v1.4.1 (25/09/2017)
- [**closed**] Issue when bundling with webpack [#172](https://github.com/broofa/mime/issues/172)
---
### v2.0.2 (15/09/2017)
- [**V2**] fs.readFileSync is not a function [#165](https://github.com/broofa/mime/issues/165)
- [**closed**] The extension for video/quicktime should map to .mov, not .qt [#164](https://github.com/broofa/mime/issues/164)
- [**V2**] [v2 Feedback request] Mime class API [#163](https://github.com/broofa/mime/issues/163)
- [**V2**] [v2 Feedback request] Resolving conflicts over extensions [#162](https://github.com/broofa/mime/issues/162)
- [**V2**] Allow callers to load module with official, full, or no defined types. [#161](https://github.com/broofa/mime/issues/161)
- [**V2**] Use "facets" to resolve extension conflicts [#160](https://github.com/broofa/mime/issues/160)
- [**V2**] Remove fs and path dependencies [#152](https://github.com/broofa/mime/issues/152)
- [**V2**] Default content-type should not be application/octet-stream [#139](https://github.com/broofa/mime/issues/139)
- [**V2**] reset mime-types [#124](https://github.com/broofa/mime/issues/124)
- [**V2**] Extensionless paths should return null or false [#113](https://github.com/broofa/mime/issues/113)
---
### v2.0.1 (14/09/2017)
- [**closed**] Changelog for v2.0 does not mention breaking changes [#171](https://github.com/broofa/mime/issues/171)
- [**closed**] MIME breaking with 'class' declaration as it is without 'use strict mode' [#170](https://github.com/broofa/mime/issues/170)
---
## v2.0.0 (12/09/2017)
- [**closed**] woff and woff2 [#168](https://github.com/broofa/mime/issues/168)
---
## v1.4.0 (28/08/2017)
- [**closed**] support for ac3 voc files [#159](https://github.com/broofa/mime/issues/159)
- [**closed**] Help understanding change from application/xml to text/xml [#158](https://github.com/broofa/mime/issues/158)
- [**closed**] no longer able to override mimetype [#157](https://github.com/broofa/mime/issues/157)
- [**closed**] application/vnd.adobe.photoshop [#147](https://github.com/broofa/mime/issues/147)
- [**closed**] Directories should appear as something other than application/octet-stream [#135](https://github.com/broofa/mime/issues/135)
- [**closed**] requested features [#131](https://github.com/broofa/mime/issues/131)
- [**closed**] Make types.json loading optional? [#129](https://github.com/broofa/mime/issues/129)
- [**closed**] Cannot find module './types.json' [#120](https://github.com/broofa/mime/issues/120)
- [**V2**] .wav files show up as "audio/x-wav" instead of "audio/x-wave" [#118](https://github.com/broofa/mime/issues/118)
- [**closed**] Don't be a pain in the ass for node community [#108](https://github.com/broofa/mime/issues/108)
- [**closed**] don't make default_type global [#78](https://github.com/broofa/mime/issues/78)
- [**closed**] mime.extension() fails if the content-type is parameterized [#74](https://github.com/broofa/mime/issues/74)
---
### v1.3.6 (11/05/2017)
- [**closed**] .md should be text/markdown as of March 2016 [#154](https://github.com/broofa/mime/issues/154)
- [**closed**] Error while installing mime [#153](https://github.com/broofa/mime/issues/153)
- [**closed**] application/manifest+json [#149](https://github.com/broofa/mime/issues/149)
- [**closed**] Dynamic adaptive streaming over HTTP (DASH) file extension typo [#141](https://github.com/broofa/mime/issues/141)
- [**closed**] charsets image/png undefined [#140](https://github.com/broofa/mime/issues/140)
- [**closed**] Mime-db dependency out of date [#130](https://github.com/broofa/mime/issues/130)
- [**closed**] how to support plist [#126](https://github.com/broofa/mime/issues/126)
- [**closed**] how does .types file format look like? [#123](https://github.com/broofa/mime/issues/123)
- [**closed**] Feature: support for expanding MIME patterns [#121](https://github.com/broofa/mime/issues/121)
- [**closed**] DEBUG_MIME doesn't work [#117](https://github.com/broofa/mime/issues/117)
---
### v1.3.4 (06/02/2015)
*No changelog for this release.*
---
### v1.3.3 (06/02/2015)
*No changelog for this release.*
---
### v1.3.1 (05/02/2015)
- [**closed**] Consider adding support for Handlebars .hbs file ending [#111](https://github.com/broofa/mime/issues/111)
- [**closed**] Consider adding support for hjson. [#110](https://github.com/broofa/mime/issues/110)
- [**closed**] Add mime type for Opus audio files [#94](https://github.com/broofa/mime/issues/94)
- [**closed**] Consider making the `Requesting New Types` information more visible [#77](https://github.com/broofa/mime/issues/77)
---
## v1.3.0 (05/02/2015)
- [**closed**] Add common name? [#114](https://github.com/broofa/mime/issues/114)
- [**closed**] application/x-yaml [#104](https://github.com/broofa/mime/issues/104)
- [**closed**] Add mime type for WOFF file format 2.0 [#102](https://github.com/broofa/mime/issues/102)
- [**closed**] application/x-msi for .msi [#99](https://github.com/broofa/mime/issues/99)
- [**closed**] Add mimetype for gettext translation files [#98](https://github.com/broofa/mime/issues/98)
- [**closed**] collaborators [#88](https://github.com/broofa/mime/issues/88)
- [**closed**] getting errot in installation of mime module...any1 can help? [#87](https://github.com/broofa/mime/issues/87)
- [**closed**] should application/json's charset be utf8? [#86](https://github.com/broofa/mime/issues/86)
- [**closed**] Add "license" and "licenses" to package.json [#81](https://github.com/broofa/mime/issues/81)
- [**closed**] lookup with extension-less file on Windows returns wrong type [#68](https://github.com/broofa/mime/issues/68)
---
### v1.2.11 (15/08/2013)
- [**closed**] Update mime.types [#65](https://github.com/broofa/mime/issues/65)
- [**closed**] Publish a new version [#63](https://github.com/broofa/mime/issues/63)
- [**closed**] README should state upfront that "application/octet-stream" is default for unknown extension [#55](https://github.com/broofa/mime/issues/55)
- [**closed**] Suggested improvement to the charset API [#52](https://github.com/broofa/mime/issues/52)
---
### v1.2.10 (25/07/2013)
- [**closed**] Mime type for woff files should be application/font-woff and not application/x-font-woff [#62](https://github.com/broofa/mime/issues/62)
- [**closed**] node.types in conflict with mime.types [#51](https://github.com/broofa/mime/issues/51)
---
### v1.2.9 (17/01/2013)
- [**closed**] Please update "mime" NPM [#49](https://github.com/broofa/mime/issues/49)
- [**closed**] Please add semicolon [#46](https://github.com/broofa/mime/issues/46)
- [**closed**] parse full mime types [#43](https://github.com/broofa/mime/issues/43)
---
### v1.2.8 (10/01/2013)
- [**closed**] /js directory mime is application/javascript. Is it correct? [#47](https://github.com/broofa/mime/issues/47)
- [**closed**] Add mime types for lua code. [#45](https://github.com/broofa/mime/issues/45)
---
### v1.2.7 (19/10/2012)
- [**closed**] cannot install 1.2.7 via npm [#41](https://github.com/broofa/mime/issues/41)
- [**closed**] Transfer ownership to @broofa [#36](https://github.com/broofa/mime/issues/36)
- [**closed**] it's wrong to set charset to UTF-8 for text [#30](https://github.com/broofa/mime/issues/30)
- [**closed**] Allow multiple instances of MIME types container [#27](https://github.com/broofa/mime/issues/27)
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2010 Benjamin Thomas, Robert Kieffer
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.
+97
View File
@@ -0,0 +1,97 @@
'use strict';
/**
* @param typeMap [Object] Map of MIME type -> Array[extensions]
* @param ...
*/
function Mime() {
this._types = Object.create(null);
this._extensions = Object.create(null);
for (let i = 0; i < arguments.length; i++) {
this.define(arguments[i]);
}
this.define = this.define.bind(this);
this.getType = this.getType.bind(this);
this.getExtension = this.getExtension.bind(this);
}
/**
* Define mimetype -> extension mappings. Each key is a mime-type that maps
* to an array of extensions associated with the type. The first extension is
* used as the default extension for the type.
*
* e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']});
*
* If a type declares an extension that has already been defined, an error will
* be thrown. To suppress this error and force the extension to be associated
* with the new type, pass `force`=true. Alternatively, you may prefix the
* extension with "*" to map the type to extension, without mapping the
* extension to the type.
*
* e.g. mime.define({'audio/wav', ['wav']}, {'audio/x-wav', ['*wav']});
*
*
* @param map (Object) type definitions
* @param force (Boolean) if true, force overriding of existing definitions
*/
Mime.prototype.define = function(typeMap, force) {
for (let type in typeMap) {
let extensions = typeMap[type].map(function(t) {
return t.toLowerCase();
});
type = type.toLowerCase();
for (let i = 0; i < extensions.length; i++) {
const ext = extensions[i];
// '*' prefix = not the preferred type for this extension. So fixup the
// extension, and skip it.
if (ext[0] === '*') {
continue;
}
if (!force && (ext in this._types)) {
throw new Error(
'Attempt to change mapping for "' + ext +
'" extension from "' + this._types[ext] + '" to "' + type +
'". Pass `force=true` to allow this, otherwise remove "' + ext +
'" from the list of extensions for "' + type + '".'
);
}
this._types[ext] = type;
}
// Use first extension as default
if (force || !this._extensions[type]) {
const ext = extensions[0];
this._extensions[type] = (ext[0] !== '*') ? ext : ext.substr(1);
}
}
};
/**
* Lookup a mime type based on extension
*/
Mime.prototype.getType = function(path) {
path = String(path);
let last = path.replace(/^.*[/\\]/, '').toLowerCase();
let ext = last.replace(/^.*\./, '').toLowerCase();
let hasPath = last.length < path.length;
let hasDot = ext.length < last.length - 1;
return (hasDot || !hasPath) && this._types[ext] || null;
};
/**
* Return file extension associated with a mime type
*/
Mime.prototype.getExtension = function(type) {
type = /^\s*([^;\s]*)/.test(type) && RegExp.$1;
return type && this._extensions[type.toLowerCase()] || null;
};
module.exports = Mime;
+187
View File
@@ -0,0 +1,187 @@
<!--
-- This file is auto-generated from src/README_js.md. Changes should be made there.
-->
# Mime
A comprehensive, compact MIME type module.
[![Build Status](https://travis-ci.org/broofa/mime.svg?branch=master)](https://travis-ci.org/broofa/mime)
## Version 2 Notes
Version 2 is a breaking change from 1.x as the semver implies. Specifically:
* `lookup()` renamed to `getType()`
* `extension()` renamed to `getExtension()`
* `charset()` and `load()` methods have been removed
If you prefer the legacy version of this module please `npm install mime@^1`. Version 1 docs may be found [here](https://github.com/broofa/mime/tree/v1.4.0).
## Install
### NPM
```
npm install mime
```
### Browser
It is recommended that you use a bundler such as
[webpack](https://webpack.github.io/) or [browserify](http://browserify.org/) to
package your code. However, browser-ready versions are available via wzrd.in.
E.g. For the full version:
<script src="https://wzrd.in/standalone/mime@latest"></script>
<script>
mime.getType(...); // etc.
</script>
Or, for the `mime/lite` version:
<script src="https://wzrd.in/standalone/mime%2flite@latest"></script>
<script>
mimelite.getType(...); // (Note `mimelite` here)
</script>
## Quick Start
For the full version (800+ MIME types, 1,000+ extensions):
```javascript
const mime = require('mime');
mime.getType('txt'); // ⇨ 'text/plain'
mime.getExtension('text/plain'); // ⇨ 'txt'
```
See [Mime API](#mime-api) below for API details.
## Lite Version
There is also a "lite" version of this module that omits vendor-specific
(`*/vnd.*`) and experimental (`*/x-*`) types. It weighs in at ~2.5KB, compared
to 8KB for the full version. To load the lite version:
```javascript
const mime = require('mime/lite');
```
## Mime .vs. mime-types .vs. mime-db modules
For those of you wondering about the difference between these [popular] NPM modules,
here's a brief rundown ...
[`mime-db`](https://github.com/jshttp/mime-db) is "the source of
truth" for MIME type information. It is not an API. Rather, it is a canonical
dataset of mime type definitions pulled from IANA, Apache, NGINX, and custom mappings
submitted by the Node.js community.
[`mime-types`](https://github.com/jshttp/mime-types) is a thin
wrapper around mime-db that provides an API drop-in compatible(ish) with `mime @ < v1.3.6` API.
`mime` is, as of v2, a self-contained module bundled with a pre-optimized version
of the `mime-db` dataset. It provides a simplified API with the following characteristics:
* Intelligently resolved type conflicts (See [mime-score](https://github.com/broofa/mime-score) for details)
* Method naming consistent with industry best-practices
* Compact footprint. E.g. The minified+compressed sizes of the various modules:
Module | Size
--- | ---
`mime-db` | 18 KB
`mime-types` | same as mime-db
`mime` | 8 KB
`mime/lite` | 2 KB
## Mime API
Both `require('mime')` and `require('mime/lite')` return instances of the MIME
class, documented below.
Note: Inputs to this API are case-insensitive. Outputs (returned values) will
be lowercase.
### new Mime(typeMap, ... more maps)
Most users of this module will not need to create Mime instances directly.
However if you would like to create custom mappings, you may do so as follows
...
```javascript
// Require Mime class
const Mime = require('mime/Mime');
// Define mime type -> extensions map
const typeMap = {
'text/abc': ['abc', 'alpha', 'bet'],
'text/def': ['leppard']
};
// Create and use Mime instance
const myMime = new Mime(typeMap);
myMime.getType('abc'); // ⇨ 'text/abc'
myMime.getExtension('text/def'); // ⇨ 'leppard'
```
If more than one map argument is provided, each map is `define()`ed (see below), in order.
### mime.getType(pathOrExtension)
Get mime type for the given path or extension. E.g.
```javascript
mime.getType('js'); // ⇨ 'application/javascript'
mime.getType('json'); // ⇨ 'application/json'
mime.getType('txt'); // ⇨ 'text/plain'
mime.getType('dir/text.txt'); // ⇨ 'text/plain'
mime.getType('dir\\text.txt'); // ⇨ 'text/plain'
mime.getType('.text.txt'); // ⇨ 'text/plain'
mime.getType('.txt'); // ⇨ 'text/plain'
```
`null` is returned in cases where an extension is not detected or recognized
```javascript
mime.getType('foo/txt'); // ⇨ null
mime.getType('bogus_type'); // ⇨ null
```
### mime.getExtension(type)
Get extension for the given mime type. Charset options (often included in
Content-Type headers) are ignored.
```javascript
mime.getExtension('text/plain'); // ⇨ 'txt'
mime.getExtension('application/json'); // ⇨ 'json'
mime.getExtension('text/html; charset=utf8'); // ⇨ 'html'
```
### mime.define(typeMap[, force = false])
Define [more] type mappings.
`typeMap` is a map of type -> extensions, as documented in `new Mime`, above.
By default this method will throw an error if you try to map a type to an
extension that is already assigned to another type. Passing `true` for the
`force` argument will suppress this behavior (overriding any previous mapping).
```javascript
mime.define({'text/x-abc': ['abc', 'abcd']});
mime.getType('abcd'); // ⇨ 'text/x-abc'
mime.getExtension('text/x-abc') // ⇨ 'abc'
```
## Command Line
mime [path_or_extension]
E.g.
> mime scripts/jquery.js
application/javascript
----
Markdown generated from [src/README_js.md](src/README_js.md) by [![RunMD Logo](http://i.imgur.com/h0FVyzU.png)](https://github.com/broofa/runmd)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env node
'use strict';
process.title = 'mime';
let mime = require('.');
let pkg = require('./package.json');
let args = process.argv.splice(2);
if (args.includes('--version') || args.includes('-v') || args.includes('--v')) {
console.log(pkg.version);
process.exit(0);
} else if (args.includes('--name') || args.includes('-n') || args.includes('--n')) {
console.log(pkg.name);
process.exit(0);
} else if (args.includes('--help') || args.includes('-h') || args.includes('--h')) {
console.log(pkg.name + ' - ' + pkg.description + '\n');
console.log(`Usage:
mime [flags] [path_or_extension]
Flags:
--help, -h Show this message
--version, -v Display the version
--name, -n Print the name of the program
Note: the command will exit after it executes if a command is specified
The path_or_extension is the path to the file or the extension of the file.
Examples:
mime --help
mime --version
mime --name
mime -v
mime src/log.js
mime new.py
mime foo.sh
`);
process.exit(0);
}
let file = args[0];
let type = mime.getType(file);
process.stdout.write(type + '\n');
+4
View File
@@ -0,0 +1,4 @@
'use strict';
let Mime = require('./Mime');
module.exports = new Mime(require('./types/standard'), require('./types/other'));
+4
View File
@@ -0,0 +1,4 @@
'use strict';
let Mime = require('./Mime');
module.exports = new Mime(require('./types/standard'));
+52
View File
@@ -0,0 +1,52 @@
{
"author": {
"name": "Robert Kieffer",
"url": "http://github.com/broofa",
"email": "robert@broofa.com"
},
"engines": {
"node": ">=4.0.0"
},
"bin": {
"mime": "cli.js"
},
"contributors": [],
"description": "A comprehensive library for mime-type mapping",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"benchmark": "*",
"chalk": "4.1.2",
"eslint": "8.1.0",
"mime-db": "1.50.0",
"mime-score": "1.2.0",
"mime-types": "2.1.33",
"mocha": "9.1.3",
"runmd": "*",
"standard-version": "9.3.2"
},
"files": [
"index.js",
"lite.js",
"Mime.js",
"cli.js",
"/types"
],
"scripts": {
"prepare": "node src/build.js && runmd --output README.md src/README_js.md",
"release": "standard-version",
"benchmark": "node src/benchmark.js",
"md": "runmd --watch --output README.md src/README_js.md",
"test": "mocha src/test.js"
},
"keywords": [
"util",
"mime"
],
"name": "mime",
"repository": {
"url": "https://github.com/broofa/mime",
"type": "git"
},
"version": "2.6.0"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+65
View File
@@ -0,0 +1,65 @@
# v3.0
- Add `--preserve-root` option to executable (default true)
- Drop support for Node.js below version 6
# v2.7
- Make `glob` an optional dependency
# 2.6
- Retry on EBUSY on non-windows platforms as well
- Make `rimraf.sync` 10000% more reliable on Windows
# 2.5
- Handle Windows EPERM when lstat-ing read-only dirs
- Add glob option to pass options to glob
# 2.4
- Add EPERM to delay/retry loop
- Add `disableGlob` option
# 2.3
- Make maxBusyTries and emfileWait configurable
- Handle weird SunOS unlink-dir issue
- Glob the CLI arg for better Windows support
# 2.2
- Handle ENOENT properly on Windows
- Allow overriding fs methods
- Treat EPERM as indicative of non-empty dir
- Remove optional graceful-fs dep
- Consistently return null error instead of undefined on success
- win32: Treat ENOTEMPTY the same as EBUSY
- Add `rimraf` binary
# 2.1
- Fix SunOS error code for a non-empty directory
- Try rmdir before readdir
- Treat EISDIR like EPERM
- Remove chmod
- Remove lstat polyfill, node 0.7 is not supported
# 2.0
- Fix myGid call to check process.getgid
- Simplify the EBUSY backoff logic.
- Use fs.lstat in node >= 0.7.9
- Remove gently option
- remove fiber implementation
- Delete files that are marked read-only
# 1.0
- Allow ENOENT in sync method
- Throw when no callback is provided
- Make opts.gently an absolute path
- use 'stat' if 'lstat' is not available
- Consistent error naming, and rethrow non-ENOENT stat errors
- add fiber implementation
+15
View File
@@ -0,0 +1,15 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+101
View File
@@ -0,0 +1,101 @@
[![Build Status](https://travis-ci.org/isaacs/rimraf.svg?branch=master)](https://travis-ci.org/isaacs/rimraf) [![Dependency Status](https://david-dm.org/isaacs/rimraf.svg)](https://david-dm.org/isaacs/rimraf) [![devDependency Status](https://david-dm.org/isaacs/rimraf/dev-status.svg)](https://david-dm.org/isaacs/rimraf#info=devDependencies)
The [UNIX command](http://en.wikipedia.org/wiki/Rm_(Unix)) `rm -rf` for node.
Install with `npm install rimraf`, or just drop rimraf.js somewhere.
## API
`rimraf(f, [opts], callback)`
The first parameter will be interpreted as a globbing pattern for files. If you
want to disable globbing you can do so with `opts.disableGlob` (defaults to
`false`). This might be handy, for instance, if you have filenames that contain
globbing wildcard characters.
The callback will be called with an error if there is one. Certain
errors are handled for you:
* Windows: `EBUSY` and `ENOTEMPTY` - rimraf will back off a maximum of
`opts.maxBusyTries` times before giving up, adding 100ms of wait
between each attempt. The default `maxBusyTries` is 3.
* `ENOENT` - If the file doesn't exist, rimraf will return
successfully, since your desired outcome is already the case.
* `EMFILE` - Since `readdir` requires opening a file descriptor, it's
possible to hit `EMFILE` if too many file descriptors are in use.
In the sync case, there's nothing to be done for this. But in the
async case, rimraf will gradually back off with timeouts up to
`opts.emfileWait` ms, which defaults to 1000.
## options
* unlink, chmod, stat, lstat, rmdir, readdir,
unlinkSync, chmodSync, statSync, lstatSync, rmdirSync, readdirSync
In order to use a custom file system library, you can override
specific fs functions on the options object.
If any of these functions are present on the options object, then
the supplied function will be used instead of the default fs
method.
Sync methods are only relevant for `rimraf.sync()`, of course.
For example:
```javascript
var myCustomFS = require('some-custom-fs')
rimraf('some-thing', myCustomFS, callback)
```
* maxBusyTries
If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error code is encountered
on Windows systems, then rimraf will retry with a linear backoff
wait of 100ms longer on each try. The default maxBusyTries is 3.
Only relevant for async usage.
* emfileWait
If an `EMFILE` error is encountered, then rimraf will retry
repeatedly with a linear backoff of 1ms longer on each try, until
the timeout counter hits this max. The default limit is 1000.
If you repeatedly encounter `EMFILE` errors, then consider using
[graceful-fs](http://npm.im/graceful-fs) in your program.
Only relevant for async usage.
* glob
Set to `false` to disable [glob](http://npm.im/glob) pattern
matching.
Set to an object to pass options to the glob module. The default
glob options are `{ nosort: true, silent: true }`.
Glob version 6 is used in this module.
Relevant for both sync and async usage.
* disableGlob
Set to any non-falsey value to disable globbing entirely.
(Equivalent to setting `glob: false`.)
## rimraf.sync
It can remove stuff synchronously, too. But that's not so good. Use
the async API. It's better.
## CLI
If installed with `npm install rimraf -g` it can be used as a global
command `rimraf <path> [<path> ...]` which is useful for cross platform support.
## mkdirp
If you need to create a directory recursively, check out
[mkdirp](https://github.com/substack/node-mkdirp).
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env node
const rimraf = require('./')
const path = require('path')
const isRoot = arg => /^(\/|[a-zA-Z]:\\)$/.test(path.resolve(arg))
const filterOutRoot = arg => {
const ok = preserveRoot === false || !isRoot(arg)
if (!ok) {
console.error(`refusing to remove ${arg}`)
console.error('Set --no-preserve-root to allow this')
}
return ok
}
let help = false
let dashdash = false
let noglob = false
let preserveRoot = true
const args = process.argv.slice(2).filter(arg => {
if (dashdash)
return !!arg
else if (arg === '--')
dashdash = true
else if (arg === '--no-glob' || arg === '-G')
noglob = true
else if (arg === '--glob' || arg === '-g')
noglob = false
else if (arg.match(/^(-+|\/)(h(elp)?|\?)$/))
help = true
else if (arg === '--preserve-root')
preserveRoot = true
else if (arg === '--no-preserve-root')
preserveRoot = false
else
return !!arg
}).filter(arg => !preserveRoot || filterOutRoot(arg))
const go = n => {
if (n >= args.length)
return
const options = noglob ? { glob: false } : {}
rimraf(args[n], options, er => {
if (er)
throw er
go(n+1)
})
}
if (help || args.length === 0) {
// If they didn't ask for help, then this is not a "success"
const log = help ? console.log : console.error
log('Usage: rimraf <path> [<path> ...]')
log('')
log(' Deletes all files and folders at "path" recursively.')
log('')
log('Options:')
log('')
log(' -h, --help Display this usage info')
log(' -G, --no-glob Do not expand glob patterns in arguments')
log(' -g, --glob Expand glob patterns in arguments (default)')
log(' --preserve-root Do not remove \'/\' (default)')
log(' --no-preserve-root Do not treat \'/\' specially')
log(' -- Stop parsing flags')
process.exit(help ? 0 : 1)
} else
go(0)
+32
View File
@@ -0,0 +1,32 @@
{
"name": "rimraf",
"version": "3.0.2",
"main": "rimraf.js",
"description": "A deep deletion module for node (like `rm -rf`)",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"license": "ISC",
"repository": "git://github.com/isaacs/rimraf.git",
"scripts": {
"preversion": "npm test",
"postversion": "npm publish",
"postpublish": "git push origin --follow-tags",
"test": "tap test/*.js"
},
"bin": "./bin.js",
"dependencies": {
"glob": "^7.1.3"
},
"files": [
"LICENSE",
"README.md",
"bin.js",
"rimraf.js"
],
"devDependencies": {
"mkdirp": "^0.5.1",
"tap": "^12.1.1"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
}
+360
View File
@@ -0,0 +1,360 @@
const assert = require("assert")
const path = require("path")
const fs = require("fs")
let glob = undefined
try {
glob = require("glob")
} catch (_err) {
// treat glob as optional.
}
const defaultGlobOpts = {
nosort: true,
silent: true
}
// for EMFILE handling
let timeout = 0
const isWindows = (process.platform === "win32")
const defaults = options => {
const methods = [
'unlink',
'chmod',
'stat',
'lstat',
'rmdir',
'readdir'
]
methods.forEach(m => {
options[m] = options[m] || fs[m]
m = m + 'Sync'
options[m] = options[m] || fs[m]
})
options.maxBusyTries = options.maxBusyTries || 3
options.emfileWait = options.emfileWait || 1000
if (options.glob === false) {
options.disableGlob = true
}
if (options.disableGlob !== true && glob === undefined) {
throw Error('glob dependency not found, set `options.disableGlob = true` if intentional')
}
options.disableGlob = options.disableGlob || false
options.glob = options.glob || defaultGlobOpts
}
const rimraf = (p, options, cb) => {
if (typeof options === 'function') {
cb = options
options = {}
}
assert(p, 'rimraf: missing path')
assert.equal(typeof p, 'string', 'rimraf: path should be a string')
assert.equal(typeof cb, 'function', 'rimraf: callback function required')
assert(options, 'rimraf: invalid options argument provided')
assert.equal(typeof options, 'object', 'rimraf: options should be object')
defaults(options)
let busyTries = 0
let errState = null
let n = 0
const next = (er) => {
errState = errState || er
if (--n === 0)
cb(errState)
}
const afterGlob = (er, results) => {
if (er)
return cb(er)
n = results.length
if (n === 0)
return cb()
results.forEach(p => {
const CB = (er) => {
if (er) {
if ((er.code === "EBUSY" || er.code === "ENOTEMPTY" || er.code === "EPERM") &&
busyTries < options.maxBusyTries) {
busyTries ++
// try again, with the same exact callback as this one.
return setTimeout(() => rimraf_(p, options, CB), busyTries * 100)
}
// this one won't happen if graceful-fs is used.
if (er.code === "EMFILE" && timeout < options.emfileWait) {
return setTimeout(() => rimraf_(p, options, CB), timeout ++)
}
// already gone
if (er.code === "ENOENT") er = null
}
timeout = 0
next(er)
}
rimraf_(p, options, CB)
})
}
if (options.disableGlob || !glob.hasMagic(p))
return afterGlob(null, [p])
options.lstat(p, (er, stat) => {
if (!er)
return afterGlob(null, [p])
glob(p, options.glob, afterGlob)
})
}
// Two possible strategies.
// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR
// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR
//
// Both result in an extra syscall when you guess wrong. However, there
// are likely far more normal files in the world than directories. This
// is based on the assumption that a the average number of files per
// directory is >= 1.
//
// If anyone ever complains about this, then I guess the strategy could
// be made configurable somehow. But until then, YAGNI.
const rimraf_ = (p, options, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
// sunos lets the root user unlink directories, which is... weird.
// so we have to lstat here and make sure it's not a dir.
options.lstat(p, (er, st) => {
if (er && er.code === "ENOENT")
return cb(null)
// Windows can EPERM on stat. Life is suffering.
if (er && er.code === "EPERM" && isWindows)
fixWinEPERM(p, options, er, cb)
if (st && st.isDirectory())
return rmdir(p, options, er, cb)
options.unlink(p, er => {
if (er) {
if (er.code === "ENOENT")
return cb(null)
if (er.code === "EPERM")
return (isWindows)
? fixWinEPERM(p, options, er, cb)
: rmdir(p, options, er, cb)
if (er.code === "EISDIR")
return rmdir(p, options, er, cb)
}
return cb(er)
})
})
}
const fixWinEPERM = (p, options, er, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
options.chmod(p, 0o666, er2 => {
if (er2)
cb(er2.code === "ENOENT" ? null : er)
else
options.stat(p, (er3, stats) => {
if (er3)
cb(er3.code === "ENOENT" ? null : er)
else if (stats.isDirectory())
rmdir(p, options, er, cb)
else
options.unlink(p, cb)
})
})
}
const fixWinEPERMSync = (p, options, er) => {
assert(p)
assert(options)
try {
options.chmodSync(p, 0o666)
} catch (er2) {
if (er2.code === "ENOENT")
return
else
throw er
}
let stats
try {
stats = options.statSync(p)
} catch (er3) {
if (er3.code === "ENOENT")
return
else
throw er
}
if (stats.isDirectory())
rmdirSync(p, options, er)
else
options.unlinkSync(p)
}
const rmdir = (p, options, originalEr, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
// try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS)
// if we guessed wrong, and it's not a directory, then
// raise the original error.
options.rmdir(p, er => {
if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM"))
rmkids(p, options, cb)
else if (er && er.code === "ENOTDIR")
cb(originalEr)
else
cb(er)
})
}
const rmkids = (p, options, cb) => {
assert(p)
assert(options)
assert(typeof cb === 'function')
options.readdir(p, (er, files) => {
if (er)
return cb(er)
let n = files.length
if (n === 0)
return options.rmdir(p, cb)
let errState
files.forEach(f => {
rimraf(path.join(p, f), options, er => {
if (errState)
return
if (er)
return cb(errState = er)
if (--n === 0)
options.rmdir(p, cb)
})
})
})
}
// this looks simpler, and is strictly *faster*, but will
// tie up the JavaScript thread and fail on excessively
// deep directory trees.
const rimrafSync = (p, options) => {
options = options || {}
defaults(options)
assert(p, 'rimraf: missing path')
assert.equal(typeof p, 'string', 'rimraf: path should be a string')
assert(options, 'rimraf: missing options')
assert.equal(typeof options, 'object', 'rimraf: options should be object')
let results
if (options.disableGlob || !glob.hasMagic(p)) {
results = [p]
} else {
try {
options.lstatSync(p)
results = [p]
} catch (er) {
results = glob.sync(p, options.glob)
}
}
if (!results.length)
return
for (let i = 0; i < results.length; i++) {
const p = results[i]
let st
try {
st = options.lstatSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
// Windows can EPERM on stat. Life is suffering.
if (er.code === "EPERM" && isWindows)
fixWinEPERMSync(p, options, er)
}
try {
// sunos lets the root user unlink directories, which is... weird.
if (st && st.isDirectory())
rmdirSync(p, options, null)
else
options.unlinkSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
if (er.code === "EPERM")
return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er)
if (er.code !== "EISDIR")
throw er
rmdirSync(p, options, er)
}
}
}
const rmdirSync = (p, options, originalEr) => {
assert(p)
assert(options)
try {
options.rmdirSync(p)
} catch (er) {
if (er.code === "ENOENT")
return
if (er.code === "ENOTDIR")
throw originalEr
if (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM")
rmkidsSync(p, options)
}
}
const rmkidsSync = (p, options) => {
assert(p)
assert(options)
options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options))
// We only end up here once we got ENOTEMPTY at least once, and
// at this point, we are guaranteed to have removed all the kids.
// So, we know that it won't be ENOENT or ENOTDIR or anything else.
// try really hard to delete stuff on windows, because it has a
// PROFOUNDLY annoying habit of not closing handles promptly when
// files are deleted, resulting in spurious ENOTEMPTY errors.
const retries = isWindows ? 100 : 1
let i = 0
do {
let threw = true
try {
const ret = options.rmdirSync(p, options)
threw = false
return ret
} finally {
if (++i < retries && threw)
continue
}
} while (true)
}
module.exports = rimraf
rimraf.sync = rimrafSync
+96
View File
@@ -0,0 +1,96 @@
{
"name": "puppeteer",
"version": "3.3.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"main": "index.js",
"repository": "github:puppeteer/puppeteer",
"engines": {
"node": ">=10.18.1"
},
"puppeteer": {
"chromium_revision": "756035",
"firefox_revision": "latest"
},
"scripts": {
"unit": "mocha --config mocha-config/puppeteer-unit-tests.js",
"unit-with-coverage": "cross-env COVERAGE=1 npm run unit",
"assert-unit-coverage": "cross-env COVERAGE=1 mocha --config mocha-config/coverage-tests.js",
"funit": "PUPPETEER_PRODUCT=firefox npm run unit",
"debug-unit": "node --inspect-brk test/test.js",
"test-doclint": "mocha --config mocha-config/doclint-tests.js",
"test": "npm run tsc && npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types",
"prepare": "node typescript-if-required.js",
"prepublishOnly": "npm run tsc",
"dev-install": "npm run tsc && node install.js",
"install": "node install.js",
"eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"eslint-fix": "eslint --ext js --ext ts --fix .",
"lint": "npm run eslint && npm run tsc && npm run doc",
"doc": "node utils/doclint/cli.js",
"tsc": "tsc --version && tsc -p . && cp src/protocol.d.ts lib/",
"apply-next-version": "node utils/apply_next_version.js",
"update-protocol-d-ts": "node utils/protocol-types-generator update",
"compare-protocol-d-ts": "node utils/protocol-types-generator compare",
"test-install": "scripts/test-install.sh"
},
"files": [
"lib/",
"Errors.js",
"DeviceDescriptors.js",
"index.js",
"install.js",
"typescript-if-required.js"
],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"mime": "^2.0.3",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
},
"devDependencies": {
"@types/debug": "0.0.31",
"@types/mime": "^2.0.0",
"@types/node": "^10.17.14",
"@types/proxy-from-env": "^1.0.1",
"@types/rimraf": "^2.0.2",
"@types/tar-fs": "^1.16.2",
"@types/ws": "^7.2.4",
"@typescript-eslint/eslint-plugin": "^2.28.0",
"@typescript-eslint/parser": "^2.28.0",
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-unicorn": "^19.0.1",
"esprima": "^4.0.0",
"expect": "^25.2.7",
"jpeg-js": "^0.3.7",
"minimist": "^1.2.0",
"mocha": "^7.1.1",
"ncp": "^2.0.0",
"pixelmatch": "^4.0.2",
"pngjs": "^5.0.0",
"prettier": "^2.0.5",
"sinon": "^9.0.2",
"text-diff": "^1.0.1",
"typescript": "3.9.2"
},
"browser": {
"./lib/BrowserFetcher.js": false,
"ws": "./utils/browser/WebSocket",
"fs": false,
"child_process": false,
"rimraf": false,
"readline": false
}
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const child_process = require('child_process');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const exec = promisify(child_process.exec);
const fsAccess = promisify(fs.access);
const fileExists = async (filePath) =>
fsAccess(filePath)
.then(() => true)
.catch(() => false);
/*
* Now Puppeteer is built with TypeScript, we need to ensure that
* locally we have the generated output before trying to install.
*
* For users installing puppeteer this is fine, they will have the
* generated lib/ directory as we ship it when we publish to npm.
*
* However, if you're cloning the repo to contribute, you won't have the
* generated lib/ directory so this script checks if we need to run
* TypeScript first to ensure the output exists and is in the right
* place.
*/
async function compileTypeScript() {
return exec('npm run tsc').catch((error) => {
console.error('Error running TypeScript', error);
process.exit(1);
});
}
async function compileTypeScriptIfRequired() {
const libPath = path.join(__dirname, 'lib');
const libExists = await fileExists(libPath);
if (libExists) return;
console.log('Puppeteer:', 'Compiling TypeScript...');
await compileTypeScript();
}
// It's being run as node typescript-if-required.js, not require('..')
if (require.main === module) compileTypeScriptIfRequired();
module.exports = compileTypeScriptIfRequired;