зеркало из https://github.com/mozilla/gecko-dev.git
bug 1540655: remote: vendor Puppeteer; r=remote-protocol-reviewers,jdescottes
Puppeteer is licensed under the Apache-2.0 license. No code from Puppeteer gets included in Firefox. Differential Revision: https://phabricator.services.mozilla.com/D37008 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
1c16637ab1
Коммит
39ff053bb9
|
@ -0,0 +1,18 @@
|
|||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "6.12.3"
|
||||
- nodejs_version: "8.11.3"
|
||||
|
||||
build: off
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
- if "%nodejs_version%" == "8.11.3" (
|
||||
npm run lint &&
|
||||
npm run coverage &&
|
||||
npm run test-doclint &&
|
||||
npm run test-types
|
||||
) else (
|
||||
npm run unit-node6
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:6.12.3
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
||||
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:8.11.3
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
||||
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
|
@ -0,0 +1,47 @@
|
|||
env:
|
||||
DISPLAY: :99.0
|
||||
|
||||
task:
|
||||
matrix:
|
||||
- name: Chromium (node6 + linux)
|
||||
container:
|
||||
dockerfile: .ci/node6/Dockerfile.linux
|
||||
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
|
||||
install_script: npm install --unsafe-perm
|
||||
test_script: npm run unit-node6
|
||||
|
||||
task:
|
||||
matrix:
|
||||
- name: Chromium (node8 + linux)
|
||||
container:
|
||||
dockerfile: .ci/node8/Dockerfile.linux
|
||||
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
|
||||
install_script: npm install --unsafe-perm
|
||||
lint_script: npm run lint
|
||||
coverage_script: npm run coverage
|
||||
test_doclint_script: npm run test-doclint
|
||||
test_types_script: npm run test-types
|
||||
|
||||
task:
|
||||
matrix:
|
||||
- name: Firefox (node8 + linux)
|
||||
container:
|
||||
dockerfile: .ci/node8/Dockerfile.linux
|
||||
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
|
||||
install_script: npm install --unsafe-perm && cd experimental/puppeteer-firefox && npm install --unsafe-perm
|
||||
test_script: npm run funit
|
||||
|
||||
task:
|
||||
osx_instance:
|
||||
image: high-sierra-base
|
||||
name: Chromium (node8 + macOS)
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
node_install_script:
|
||||
- brew install node@8
|
||||
- brew link --force node@8
|
||||
install_script: npm install --unsafe-perm
|
||||
lint_script: npm run lint
|
||||
coverage_script: npm run coverage
|
||||
test_doclint_script: npm run test-doclint
|
||||
test_types_script: npm run test-types
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -0,0 +1,9 @@
|
|||
test/assets/modernizr.js
|
||||
third_party/*
|
||||
utils/browser/puppeteer-web.js
|
||||
utils/doclint/check_public_api/test/
|
||||
utils/testrunner/examples/
|
||||
node6/*
|
||||
node6-test/*
|
||||
node6-testrunner/*
|
||||
experimental/
|
|
@ -0,0 +1,112 @@
|
|||
module.exports = {
|
||||
"root": true,
|
||||
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9
|
||||
},
|
||||
|
||||
/**
|
||||
* ESLint rules
|
||||
*
|
||||
* All available rules: http://eslint.org/docs/rules/
|
||||
*
|
||||
* Rules take the following form:
|
||||
* "rule-name", [severity, { opts }]
|
||||
* Severity: 2 == error, 1 == warning, 0 == off.
|
||||
*/
|
||||
"rules": {
|
||||
/**
|
||||
* Enforced rules
|
||||
*/
|
||||
|
||||
|
||||
// syntax preferences
|
||||
"quotes": [2, "single", {
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}],
|
||||
"semi": 2,
|
||||
"no-extra-semi": 2,
|
||||
"comma-style": [2, "last"],
|
||||
"wrap-iife": [2, "inside"],
|
||||
"spaced-comment": [2, "always", {
|
||||
"markers": ["*"]
|
||||
}],
|
||||
"eqeqeq": [2],
|
||||
"arrow-body-style": [2, "as-needed"],
|
||||
"accessor-pairs": [2, {
|
||||
"getWithoutSet": false,
|
||||
"setWithoutGet": false
|
||||
}],
|
||||
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
|
||||
"curly": [2, "multi-or-nest", "consistent"],
|
||||
"new-parens": 2,
|
||||
"func-call-spacing": 2,
|
||||
"arrow-parens": [2, "as-needed"],
|
||||
"prefer-const": 2,
|
||||
"quote-props": [2, "consistent"],
|
||||
|
||||
// anti-patterns
|
||||
"no-var": 2,
|
||||
"no-with": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-caller": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-labels": 2,
|
||||
"no-new-object": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-cond-assign": 2,
|
||||
"no-debugger": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-empty-character-class": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unsafe-negation": 2,
|
||||
"radix": 2,
|
||||
"valid-typeof": 2,
|
||||
"no-unused-vars": [2, { "args": "none", "vars": "local", "varsIgnorePattern": "([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)" }],
|
||||
"no-implicit-globals": [2],
|
||||
|
||||
// es2015 features
|
||||
"require-yield": 2,
|
||||
"template-curly-spacing": [2, "never"],
|
||||
|
||||
// spacing details
|
||||
"space-infix-ops": 2,
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-before-function-paren": [2, "never"],
|
||||
"no-whitespace-before-property": 2,
|
||||
"keyword-spacing": [2, {
|
||||
"overrides": {
|
||||
"if": {"after": true},
|
||||
"else": {"after": true},
|
||||
"for": {"after": true},
|
||||
"while": {"after": true},
|
||||
"do": {"after": true},
|
||||
"switch": {"after": true},
|
||||
"return": {"after": true}
|
||||
}
|
||||
}],
|
||||
"arrow-spacing": [2, {
|
||||
"after": true,
|
||||
"before": true
|
||||
}],
|
||||
|
||||
// file whitespace
|
||||
"no-multiple-empty-lines": [2, {"max": 2}],
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
|
||||
"indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }],
|
||||
"key-spacing": [2, {
|
||||
"beforeColon": false
|
||||
}]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
# exclude all tests
|
||||
test
|
||||
utils/node6-transform
|
||||
|
||||
# exclude internal type definition files
|
||||
/lib/*.d.ts
|
||||
/node6/lib/*.d.ts
|
||||
|
||||
# repeats from .gitignore
|
||||
node_modules
|
||||
.local-chromium
|
||||
.dev_profile*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
package-lock.json
|
||||
/node6/test
|
||||
/node6/utils
|
||||
/test
|
||||
/utils
|
||||
/docs
|
||||
yarn.lock
|
||||
|
||||
# other
|
||||
/.ci
|
||||
/examples
|
||||
.appveyour.yml
|
||||
.cirrus.yml
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.travis.yml
|
||||
README.md
|
||||
tsconfig.json
|
||||
experimental
|
||||
|
||||
# exclude types, see https://github.com/GoogleChrome/puppeteer/issues/3878
|
||||
/index.d.ts
|
|
@ -0,0 +1,44 @@
|
|||
language: node_js
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
# This is required to run new chrome on old trusty
|
||||
- libnss3
|
||||
notifications:
|
||||
email: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
# allow headful tests
|
||||
before_install:
|
||||
- "sysctl kernel.unprivileged_userns_clone=1"
|
||||
- "export DISPLAY=:99.0"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- 'if [ "$NODE8" = "true" ]; then cd experimental/puppeteer-firefox && npm i && cd ../..; fi'
|
||||
script:
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run lint; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run coverage; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run funit; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run test-types; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run bundle; fi'
|
||||
- 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi'
|
||||
- 'if [ "$NODE6" = "true" ]; then npm run unit-node6; fi'
|
||||
jobs:
|
||||
include:
|
||||
- node_js: "8.11.3"
|
||||
env: NODE8=true
|
||||
- node_js: "6.12.3"
|
||||
env: NODE6=true
|
||||
before_deploy: "npm run apply-next-version"
|
||||
deploy:
|
||||
provider: npm
|
||||
email: aslushnikov@gmail.com
|
||||
api_key:
|
||||
secure: Ng8o2KwJf90XCBNgUKK3jRZnwtdBSJatjYNmZBERJEqBWFTadFAp1NdhxZaqjnuG8aFYaH5bRJdL+EQBYUksVCbrv/gcaXeEFkwsfPfVX1QXGqu7NnZmtme2hbxppLQ7dEJ8hz2Z9K4vehqVOxmLabxvoupOumxEQMLCphVHh2FOmsm/S5JrRZqZ4V9k76eIc0/PiyfXNMdx5WTZjHbIRDIHRy9nqOXjFp2Rx3PMa3uU2fS8mTshYEYs151TA6e6VdHjqmBwEQC/M5tXbDlLCMNUr4JBtLTcL4OipNYjzkwD1N2xYlbSRqtvqqF4ifdvFhoI65a31GinlMC7Z/SH1Zy+d+/z3Mo7D63eYcsJVnsg9OYxTFy2piUntr0JqTBHtQoe/CvGxJmkcVt+H6YSkcBibSG9s9tG3qpAD5wBCFqqOYnfClX+YZziEd+Hngd9inxAf87qdvgVIZ5tPD2dygtE+te2/qoEHtvccv/HuS8MxNj5iKwlP7JaBPM6uAkazYqZP2R99I2ph9gNOEVuQLtk+3+OIdb8HWrEKUrJBgKhdKY1dvcKYElI+D8NRlyzrr6BnZfudACuAt2EtfKpfJ3mL+iRMFdBJ3ntLt93xBrB+j4z3pD0iWZcg1g3I742PFzQEHzyd/DDTP1yRTUoJeQWwoQRJyNO1m6Qk4wx77c=
|
||||
on:
|
||||
branch: master
|
||||
condition: "$NODE8 = true"
|
||||
skip_cleanup: true
|
||||
tag: next
|
|
@ -0,0 +1,266 @@
|
|||
<!-- gen:toc -->
|
||||
- [How to Contribute](#how-to-contribute)
|
||||
* [Contributor License Agreement](#contributor-license-agreement)
|
||||
* [Getting setup](#getting-setup)
|
||||
* [Code reviews](#code-reviews)
|
||||
* [Code Style](#code-style)
|
||||
* [API guidelines](#api-guidelines)
|
||||
* [Commit Messages](#commit-messages)
|
||||
* [Writing Documentation](#writing-documentation)
|
||||
* [Adding New Dependencies](#adding-new-dependencies)
|
||||
* [Writing Tests](#writing-tests)
|
||||
* [Public API Coverage](#public-api-coverage)
|
||||
* [Debugging Puppeteer](#debugging-puppeteer)
|
||||
- [For Project Maintainers](#for-project-maintainers)
|
||||
* [Releasing to NPM](#releasing-to-npm)
|
||||
* [Updating NPM dist tags](#updating-npm-dist-tags)
|
||||
<!-- gen:stop -->
|
||||
|
||||
# How to Contribute
|
||||
|
||||
First of all, thank you for your interest in Puppeteer!
|
||||
We'd love to accept your patches and contributions!
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution,
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Getting setup
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/GoogleChrome/puppeteer
|
||||
cd puppeteer
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Coding style is fully defined in [.eslintrc](https://github.com/GoogleChrome/puppeteer/blob/master/.eslintrc.js)
|
||||
- Code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler).
|
||||
- Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
|
||||
To run code linter, use:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## API guidelines
|
||||
|
||||
When authoring new API methods, consider the following:
|
||||
- Expose as little information as needed. When in doubt, don’t expose new information.
|
||||
- Methods are used in favor of getters/setters.
|
||||
- The only exception is namespaces, e.g. `page.keyboard` and `page.coverage`
|
||||
- All string literals must be small case. This includes event names and option values.
|
||||
- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** demanded.
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Commit messages should follow the Semantic Commit Messages format:
|
||||
|
||||
```
|
||||
label(namespace): title
|
||||
|
||||
description
|
||||
|
||||
footer
|
||||
```
|
||||
|
||||
1. *label* is one of the following:
|
||||
- `fix` - puppeteer bug fixes.
|
||||
- `feat` - puppeteer features.
|
||||
- `docs` - changes to docs, e.g. `docs(api.md): ..` to change documentation.
|
||||
- `test` - changes to puppeteer tests infrastructure.
|
||||
- `style` - puppeteer code style: spaces/alignment/wrapping etc.
|
||||
- `chore` - build-related work, e.g. doclint changes / travis / appveyor.
|
||||
2. *namespace* is put in parenthesis after label and is optional.
|
||||
3. *title* is a brief summary of changes.
|
||||
4. *description* is **optional**, new-line separated from title and is in present tense.
|
||||
5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
|
||||
6. *footer* should also include "BREAKING CHANGE" if current API clients will break due to this change. It should explain what changed and how to get the old behavior.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fix(Page): fix page.pizza method
|
||||
|
||||
This patch fixes page.pizza so that it works with iframes.
|
||||
|
||||
Fixes #123, Fixes #234
|
||||
|
||||
BREAKING CHANGE: page.pizza now delivers pizza at home by default.
|
||||
To deliver to a different location, use "deliver" option:
|
||||
`page.pizza({deliver: 'work'})`.
|
||||
```
|
||||
|
||||
## Writing Documentation
|
||||
|
||||
All public API should have a descriptive entry in the [docs/api.md](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md). There's a [documentation linter](https://github.com/GoogleChrome/puppeteer/tree/master/utils/doclint) which makes sure documentation is aligned with the codebase.
|
||||
|
||||
To run documentation linter, use:
|
||||
|
||||
```bash
|
||||
npm run doc
|
||||
```
|
||||
|
||||
## Adding New Dependencies
|
||||
|
||||
For all dependencies (both installation and development):
|
||||
- **Do not add** a dependency if the desired functionality is easily implementable.
|
||||
- If adding a dependency, it should be well-maintained and trustworthy.
|
||||
|
||||
A barrier for introducing new installation dependencies is especially high:
|
||||
- **Do not add** installation dependency unless it's critical to project success.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
- Every feature should be accompanied by a test.
|
||||
- Every public api event/method should be accompanied by a test.
|
||||
- Tests should be *hermetic*. Tests should not depend on external services.
|
||||
- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests.
|
||||
|
||||
Puppeteer tests are located in [test/test.js](https://github.com/GoogleChrome/puppeteer/blob/master/test/test.js)
|
||||
and are written with a [TestRunner](https://github.com/GoogleChrome/puppeteer/tree/master/utils/testrunner) framework.
|
||||
Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected.
|
||||
|
||||
- To run all tests:
|
||||
|
||||
```bash
|
||||
npm run unit
|
||||
```
|
||||
|
||||
- To run tests in parallel, use `-j` flag:
|
||||
|
||||
```bash
|
||||
npm run unit -- -j 4
|
||||
```
|
||||
|
||||
- To run a specific test, substitute the `it` with `fit` (mnemonic rule: '*focus it*'):
|
||||
|
||||
```js
|
||||
...
|
||||
// Using "fit" to run specific test
|
||||
fit('should work', async function({server, page}) {
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok).toBe(true);
|
||||
})
|
||||
```
|
||||
|
||||
- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'):
|
||||
|
||||
```js
|
||||
...
|
||||
// Using "xit" to skip specific test
|
||||
xit('should work', async function({server, page}) {
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(response.ok).toBe(true);
|
||||
})
|
||||
```
|
||||
|
||||
- To run tests in non-headless mode:
|
||||
|
||||
```bash
|
||||
HEADLESS=false npm run unit
|
||||
```
|
||||
|
||||
- To run tests with custom Chromium executable:
|
||||
|
||||
```bash
|
||||
CHROME=<path-to-executable> npm run unit
|
||||
```
|
||||
|
||||
- To run tests in slow-mode:
|
||||
|
||||
```bash
|
||||
HEADLESS=false SLOW_MO=500 npm run unit
|
||||
```
|
||||
|
||||
- To debug a test, "focus" a test first and then run:
|
||||
|
||||
```bash
|
||||
node --inspect-brk test/test.js
|
||||
```
|
||||
|
||||
## Public API Coverage
|
||||
|
||||
Every public API method or event should be called at least once in tests. To ensure this, there's a coverage command which tracks calls to public API and reports back if some methods/events were not called.
|
||||
|
||||
Run coverage:
|
||||
|
||||
```bash
|
||||
npm run coverage
|
||||
```
|
||||
|
||||
## Debugging Puppeteer
|
||||
|
||||
See [Debugging Tips](README.md#debugging-tips) in the readme.
|
||||
|
||||
# For Project Maintainers
|
||||
|
||||
## Releasing to NPM
|
||||
|
||||
Releasing to NPM consists of 3 phases:
|
||||
1. Source Code: mark a release.
|
||||
1. Bump `package.json` version following the SEMVER rules and send a PR titled `'chore: mark version vXXX.YYY.ZZZ'` ([example](https://github.com/GoogleChrome/puppeteer/commit/808bf8e5582482a1d849ff22a51e52024810905c)).
|
||||
2. Make sure the PR passes **all checks**.
|
||||
- **WHY**: there are linters in place that help to avoid unnecessary errors, e.g. [like this](https://github.com/GoogleChrome/puppeteer/pull/2446)
|
||||
3. Merge the PR.
|
||||
4. Once merged, publish release notes using the "create new tag" option.
|
||||
- **NOTE**: tag names are prefixed with `'v'`, e.g. for version `1.4.0` tag is `v1.4.0`.
|
||||
2. Publish `puppeteer` to NPM.
|
||||
1. On your local machine, pull from [upstream](https://github.com/GoogleChrome/puppeteer) and make sure the last commit is the one just merged.
|
||||
2. Run `git status` and make sure there are no untracked files.
|
||||
- **WHY**: this is to avoid bundling unnecessary files to NPM package
|
||||
3. Run [`pkgfiles`](https://www.npmjs.com/package/pkgfiles) to make sure you don't publish anything unnecessary.
|
||||
4. Run `npm publish`. This will publish `puppeteer` package.
|
||||
3. Publish `puppeteer-core` to NPM.
|
||||
1. Run `./utils/prepare_puppeteer_core.js`. The script will change the name inside `package.json` to `puppeteer-core`.
|
||||
2. Run `npm publish`. This will publish `puppeteer-core` package.
|
||||
3. Run `git reset --hard` to reset the changes to `package.json`.
|
||||
4. Source Code: mark post-release.
|
||||
1. Bump `package.json` version to `-post` version and send a PR titled `'chore: bump version to vXXX.YYY.ZZZ-post'` ([example](https://github.com/GoogleChrome/puppeteer/commit/d02440d1eac98028e29f4e1cf55413062a259156))
|
||||
- **NOTE**: make sure to update the "released APIs" section in the top of `docs/api.md`.
|
||||
- **NOTE**: no other commits should be landed in-between release commit and bump commit.
|
||||
|
||||
## Updating NPM dist tags
|
||||
|
||||
For both `puppeteer` and `puppeteer-firefox` we maintain the following NPM Tags:
|
||||
- `chrome-*` tags, e.g. `chrome-75` and so on. These tags match Puppeteer version that corresponds to the `chrome-*` release.
|
||||
- `chrome-stable` tag. This tag points to the Puppeteer version that works with current Chrome stable.
|
||||
|
||||
These tags are updated on every Puppeteer release.
|
||||
|
||||
> **NOTE**: due to Chrome's rolling release, we take [omahaproxy's linux stable version](https://omahaproxy.appspot.com/) as *stable*.
|
||||
|
||||
Manging tags 101:
|
||||
|
||||
```bash
|
||||
# list tags
|
||||
$ npm dist-tag ls puppeteer
|
||||
# Removing a tag
|
||||
$ npm dist-tag rm puppeteer-core chrome-stable
|
||||
# Adding a tag
|
||||
$ npm dist-tag add puppeteer-core@1.13.0 chrome-stable
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let asyncawait = true;
|
||||
try {
|
||||
new Function('async function test(){await 1}');
|
||||
} catch (error) {
|
||||
asyncawait = false;
|
||||
}
|
||||
|
||||
// If node does not support async await, use the compiled version.
|
||||
if (asyncawait)
|
||||
module.exports = require('./lib/DeviceDescriptors');
|
||||
else
|
||||
module.exports = require('./node6/lib/DeviceDescriptors');
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let asyncawait = true;
|
||||
try {
|
||||
new Function('async function test(){await 1}');
|
||||
} catch (error) {
|
||||
asyncawait = false;
|
||||
}
|
||||
|
||||
// If node does not support async await, use the compiled version.
|
||||
if (asyncawait)
|
||||
module.exports = require('./lib/Errors');
|
||||
else
|
||||
module.exports = require('./node6/lib/Errors');
|
|
@ -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.
|
|
@ -0,0 +1,401 @@
|
|||
# Puppeteer
|
||||
|
||||
<!-- [START badges] -->
|
||||
[![Linux Build Status](https://img.shields.io/travis/com/GoogleChrome/puppeteer/master.svg)](https://travis-ci.com/GoogleChrome/puppeteer) [![Windows Build Status](https://img.shields.io/appveyor/ci/aslushnikov/puppeteer/master.svg?logo=appveyor)](https://ci.appveyor.com/project/aslushnikov/puppeteer/branch/master) [![Build Status](https://api.cirrus-ci.com/github/GoogleChrome/puppeteer.svg)](https://cirrus-ci.com/github/GoogleChrome/puppeteer) [![NPM puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer)
|
||||
<!-- [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/GoogleChrome/puppeteer/blob/v1.17.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/GoogleChrome/puppeteer/blob/master/CONTRIBUTING.md) | [Troubleshooting](https://github.com/GoogleChrome/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, see [Environment variables](https://github.com/GoogleChrome/puppeteer/blob/v1.17.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 Chromium 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/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteer-vs-puppeteer-core).
|
||||
|
||||
### Usage
|
||||
|
||||
Note: Puppeteer requires at least Node v6.4.0, but the 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/GoogleChrome/puppeteer/blob/v1.17.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 800px x 600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/GoogleChrome/puppeteer/blob/v1.17.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/GoogleChrome/puppeteer/blob/v1.17.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/GoogleChrome/puppeteer/blob/v1.17.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/GoogleChrome/puppeteer/blob/v1.17.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'});
|
||||
```
|
||||
|
||||
See [`Puppeteer.launch()`](https://github.com/GoogleChrome/puppeteer/blob/v1.17.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 Chromium user profile which it **cleans up on every run**.
|
||||
|
||||
<!-- [END runtimesettings] -->
|
||||
|
||||
## Resources
|
||||
|
||||
- [API Documentation](https://github.com/GoogleChrome/puppeteer/blob/v1.17.0/docs/api.md)
|
||||
- [Examples](https://github.com/GoogleChrome/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`:
|
||||
|
||||
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.
|
||||
|
||||
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()`:
|
||||
|
||||
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:
|
||||
|
||||
`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:
|
||||
|
||||
`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:
|
||||
```
|
||||
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/GoogleChrome/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/GoogleChrome/puppeteer/blob/master/CONTRIBUTING.md).
|
||||
|
||||
#### Q: What are Puppeteer’s 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 that’s 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 shouldn’t 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/GoogleChrome/puppeteer/#getting-started). At the end of the day, it’s 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. There’s 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 you’ll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment.
|
||||
|
||||
#### Q: Why doesn’t 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. Here’s a typical story:
|
||||
- A Puppeteer bug is reported: https://github.com/GoogleChrome/puppeteer/issues/2709
|
||||
- It turned out this is an issue with the DevTools protocol, so we’re 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/GoogleChrome/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/GoogleChrome/puppeteer/blob/master/package.json).
|
||||
|
||||
#### Q: What’s considered a “Navigation”?
|
||||
|
||||
From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s 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: What’s 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 it’s 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, it’s 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/GoogleChrome/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/GoogleChrome/puppeteer/blob/v1.17.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/GoogleChrome/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/GoogleChrome/puppeteer/issues)
|
||||
- [stackoverflow](https://stackoverflow.com/questions/tagged/puppeteer)
|
||||
- [slack channel](https://join.slack.com/t/puppeteer/shared_invite/enQtMzU4MjIyMDA5NTM4LTM1OTdkNDhlM2Y4ZGUzZDdjYjM5ZWZlZGFiZjc4MTkyYTVlYzIzYjU5NDIyNzgyMmFiNDFjN2UzNWU0N2ZhZDc)
|
||||
|
||||
Make sure to search these channels before posting your question.
|
||||
|
||||
|
||||
<!-- [END faq] -->
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,40 @@
|
|||
<!--
|
||||
STEP 1: Are you in the right place?
|
||||
|
||||
- For general technical questions or "how to" guidance, please search StackOverflow for questions tagged "puppeteer" or create a new post.
|
||||
|
||||
https://stackoverflow.com/questions/tagged/puppeteer
|
||||
|
||||
- For issues or feature requests related to the DevTools Protocol (https://chromedevtools.github.io/devtools-protocol/), file an issue there:
|
||||
|
||||
https://github.com/ChromeDevTools/devtools-protocol/issues/new.
|
||||
|
||||
- Problem in Headless Chrome? File an issue against Chromium's issue tracker:
|
||||
|
||||
https://bugs.chromium.org/p/chromium/issues/entry?components=Internals%3EHeadless&blocking=705916
|
||||
|
||||
For issues, feature requests, or setup troubles with Puppeteer, file an issue right here!
|
||||
-->
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
**Tell us about your environment:**
|
||||
|
||||
* Puppeteer version:
|
||||
* Platform / OS version:
|
||||
* URLs (if applicable):
|
||||
* Node.js version:
|
||||
|
||||
**What steps will reproduce the problem?**
|
||||
|
||||
_Please include code that reproduces the issue._
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**What is the expected result?**
|
||||
|
||||
|
||||
**What happens instead?**
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
# Troubleshooting
|
||||
|
||||
<!-- GEN:toc -->
|
||||
- [Chrome headless doesn't launch on Windows](#chrome-headless-doesnt-launch-on-windows)
|
||||
- [Chrome headless doesn't launch on UNIX](#chrome-headless-doesnt-launch-on-unix)
|
||||
- [Setting Up Chrome Linux Sandbox](#setting-up-chrome-linux-sandbox)
|
||||
* [[recommended] Enable user namespace cloning](#recommended-enable-user-namespace-cloning)
|
||||
* [[alternative] Setup setuid sandbox](#alternative-setup-setuid-sandbox)
|
||||
- [Running Puppeteer on Travis CI](#running-puppeteer-on-travis-ci)
|
||||
- [Running Puppeteer in Docker](#running-puppeteer-in-docker)
|
||||
* [Running on Alpine](#running-on-alpine)
|
||||
- [Tips](#tips)
|
||||
- [Running Puppeteer in the cloud](#running-puppeteer-in-the-cloud)
|
||||
* [Running Puppeteer on Google App Engine](#running-puppeteer-on-google-app-engine)
|
||||
* [Running Puppeteer on Google Cloud Functions](#running-puppeteer-on-google-cloud-functions)
|
||||
* [Running Puppeteer on Heroku](#running-puppeteer-on-heroku)
|
||||
* [Running Puppeteer on AWS Lambda](#running-puppeteer-on-aws-lambda)
|
||||
- [Code Transpilation Issues](#code-transpilation-issues)
|
||||
<!-- GEN:stop -->
|
||||
|
||||
## Chrome headless doesn't launch on Windows
|
||||
|
||||
Some [chrome policies](https://support.google.com/chrome/a/answer/7532015?hl=en) might enforce running Chrome/Chromium
|
||||
with certain extensions.
|
||||
|
||||
Puppeteer passes `--disable-extensions` flag by default and will fail to launch when such policies are active.
|
||||
|
||||
To work around this, try running without the flag:
|
||||
|
||||
```js
|
||||
const browser = await puppeteer.launch({
|
||||
ignoreDefaultArgs: ['--disable-extensions'],
|
||||
});
|
||||
```
|
||||
|
||||
> Context: [issue 3681](https://github.com/GoogleChrome/puppeteer/issues/3681#issuecomment-447865342).
|
||||
|
||||
## Chrome headless doesn't launch on UNIX
|
||||
|
||||
Make sure all the necessary dependencies are installed. You can run `ldd chrome | grep not` on a Linux
|
||||
machine to check which dependencies are missing. The common ones are provided below.
|
||||
|
||||
<details>
|
||||
<summary>Debian (e.g. Ubuntu) Dependencies</summary>
|
||||
|
||||
```
|
||||
gconf-service
|
||||
libasound2
|
||||
libatk1.0-0
|
||||
libatk-bridge2.0-0
|
||||
libc6
|
||||
libcairo2
|
||||
libcups2
|
||||
libdbus-1-3
|
||||
libexpat1
|
||||
libfontconfig1
|
||||
libgcc1
|
||||
libgconf-2-4
|
||||
libgdk-pixbuf2.0-0
|
||||
libglib2.0-0
|
||||
libgtk-3-0
|
||||
libnspr4
|
||||
libpango-1.0-0
|
||||
libpangocairo-1.0-0
|
||||
libstdc++6
|
||||
libx11-6
|
||||
libx11-xcb1
|
||||
libxcb1
|
||||
libxcomposite1
|
||||
libxcursor1
|
||||
libxdamage1
|
||||
libxext6
|
||||
libxfixes3
|
||||
libxi6
|
||||
libxrandr2
|
||||
libxrender1
|
||||
libxss1
|
||||
libxtst6
|
||||
ca-certificates
|
||||
fonts-liberation
|
||||
libappindicator1
|
||||
libnss3
|
||||
lsb-release
|
||||
xdg-utils
|
||||
wget
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CentOS Dependencies</summary>
|
||||
|
||||
```
|
||||
pango.x86_64
|
||||
libXcomposite.x86_64
|
||||
libXcursor.x86_64
|
||||
libXdamage.x86_64
|
||||
libXext.x86_64
|
||||
libXi.x86_64
|
||||
libXtst.x86_64
|
||||
cups-libs.x86_64
|
||||
libXScrnSaver.x86_64
|
||||
libXrandr.x86_64
|
||||
GConf2.x86_64
|
||||
alsa-lib.x86_64
|
||||
atk.x86_64
|
||||
gtk3.x86_64
|
||||
ipa-gothic-fonts
|
||||
xorg-x11-fonts-100dpi
|
||||
xorg-x11-fonts-75dpi
|
||||
xorg-x11-utils
|
||||
xorg-x11-fonts-cyrillic
|
||||
xorg-x11-fonts-Type1
|
||||
xorg-x11-fonts-misc
|
||||
```
|
||||
|
||||
After installing dependencies you need to update nss library using this command
|
||||
|
||||
```
|
||||
yum update nss -y
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Check out discussions</summary>
|
||||
|
||||
- [#290](https://github.com/GoogleChrome/puppeteer/issues/290) - Debian troubleshooting <br/>
|
||||
- [#391](https://github.com/GoogleChrome/puppeteer/issues/391) - CentOS troubleshooting <br/>
|
||||
- [#379](https://github.com/GoogleChrome/puppeteer/issues/379) - Alpine troubleshooting <br/>
|
||||
</details>
|
||||
|
||||
## Setting Up Chrome Linux Sandbox
|
||||
|
||||
In order to protect the host environment from untrusted web content, Chrome uses [multiple layers of sandboxing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux_sandboxing.md). For this to work properly,
|
||||
the host should be configured first. If there's no good sandbox for Chrome to use, it will crash
|
||||
with the error `No usable sandbox!`.
|
||||
|
||||
If you **absolutely trust** the content you open in Chrome, you can launch Chrome
|
||||
with the `--no-sandbox` argument:
|
||||
|
||||
```js
|
||||
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
|
||||
```
|
||||
|
||||
> **NOTE**: Running without a sandbox is **strongly discouraged**. Consider configuring a sandbox instead.
|
||||
|
||||
There are 2 ways to configure a sandbox in Chromium.
|
||||
|
||||
### [recommended] Enable [user namespace cloning](http://man7.org/linux/man-pages/man7/user_namespaces.7.html)
|
||||
|
||||
User namespace cloning is only supported by modern kernels. Unprivileged user namespaces are generally fine to enable,
|
||||
but in some cases they open up more kernel attack surface for (unsandboxed) non-root processes to elevate to
|
||||
kernel privileges.
|
||||
|
||||
```bash
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1
|
||||
```
|
||||
|
||||
### [alternative] Setup [setuid sandbox](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux_suid_sandbox_development.md)
|
||||
|
||||
The setuid sandbox comes as a standalone executable and is located next to the Chromium that Puppeteer downloads. It is
|
||||
fine to re-use the same sandbox executable for different Chromium versions, so the following could be
|
||||
done only once per host environment:
|
||||
|
||||
```bash
|
||||
# cd to the downloaded instance
|
||||
cd <project-dir-path>/node_modules/puppeteer/.local-chromium/linux-<revision>/chrome-linux/
|
||||
sudo chown root:root chrome_sandbox
|
||||
sudo chmod 4755 chrome_sandbox
|
||||
# copy sandbox executable to a shared location
|
||||
sudo cp -p chrome_sandbox /usr/local/sbin/chrome-devel-sandbox
|
||||
# export CHROME_DEVEL_SANDBOX env variable
|
||||
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox
|
||||
```
|
||||
|
||||
You might want to export the `CHROME_DEVEL_SANDBOX` env variable by default. In this case, add the following to the `~/.bashrc`
|
||||
or `.zshenv`:
|
||||
|
||||
```bash
|
||||
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox
|
||||
```
|
||||
|
||||
|
||||
## Running Puppeteer on Travis CI
|
||||
|
||||
> 👋 We run our tests for Puppeteer on Travis CI - see our [`.travis.yml`](https://github.com/GoogleChrome/puppeteer/blob/master/.travis.yml) for reference.
|
||||
|
||||
Tips-n-tricks:
|
||||
- The `libnss3` package must be installed in order to run Chromium on Ubuntu Trusty
|
||||
- [user namespace cloning](http://man7.org/linux/man-pages/man7/user_namespaces.7.html) should be enabled to support
|
||||
proper sandboxing
|
||||
- [xvfb](https://en.wikipedia.org/wiki/Xvfb) should be launched in order to run Chromium in non-headless mode (e.g. to test Chrome Extensions)
|
||||
|
||||
To sum up, your `.travis.yml` might look like this:
|
||||
|
||||
```yml
|
||||
language: node_js
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
# This is required to run new chrome on old trusty
|
||||
- libnss3
|
||||
notifications:
|
||||
email: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
# allow headful tests
|
||||
before_install:
|
||||
# Enable user namespace cloning
|
||||
- "sysctl kernel.unprivileged_userns_clone=1"
|
||||
# Launch XVFB
|
||||
- "export DISPLAY=:99.0"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
```
|
||||
|
||||
|
||||
## Running Puppeteer in Docker
|
||||
|
||||
> 👋 We use [Cirrus Ci](https://cirrus-ci.org/) to run our tests for Puppeteer in a Docker container - see our [`Dockerfile.linux`](https://github.com/GoogleChrome/puppeteer/blob/master/.ci/node8/Dockerfile.linux) for reference.
|
||||
|
||||
Getting headless Chrome up and running in Docker can be tricky.
|
||||
The bundled Chromium that Puppeteer installs is missing the necessary
|
||||
shared library dependencies.
|
||||
|
||||
To fix, you'll need to install the missing dependencies and the
|
||||
latest Chromium package in your Dockerfile:
|
||||
|
||||
```Dockerfile
|
||||
FROM node:10-slim
|
||||
|
||||
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
|
||||
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
|
||||
# installs, work.
|
||||
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
|
||||
# uncomment the following lines to have `dumb-init` as PID 1
|
||||
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
|
||||
# RUN chmod +x /usr/local/bin/dumb-init
|
||||
# ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
# Uncomment to skip the chromium download when installing puppeteer. If you do,
|
||||
# you'll need to launch puppeteer with:
|
||||
# browser.launch({executablePath: 'google-chrome-unstable'})
|
||||
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
|
||||
# Install puppeteer so it's available in the container.
|
||||
RUN npm i puppeteer \
|
||||
# Add user so we don't need --no-sandbox.
|
||||
# same layer as npm install to keep re-chowned files from using up several hundred MBs more space
|
||||
&& groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser \
|
||||
&& chown -R pptruser:pptruser /node_modules
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
||||
|
||||
CMD ["google-chrome-unstable"]
|
||||
```
|
||||
|
||||
Build the container:
|
||||
|
||||
```bash
|
||||
docker build -t puppeteer-chrome-linux .
|
||||
```
|
||||
|
||||
Run the container by passing `node -e "<yourscript.js content as a string>` as the command:
|
||||
|
||||
```bash
|
||||
docker run -i --init --rm --cap-add=SYS_ADMIN \
|
||||
--name puppeteer-chrome puppeteer-chrome-linux \
|
||||
node -e "`cat yourscript.js`"
|
||||
```
|
||||
|
||||
There's a full example at https://github.com/ebidel/try-puppeteer that shows
|
||||
how to run this Dockerfile from a webserver running on App Engine Flex (Node).
|
||||
|
||||
### Running on Alpine
|
||||
|
||||
The [newest Chromium package](https://pkgs.alpinelinux.org/package/edge/community/x86_64/chromium) supported on Alpine is 72, which was corresponding to [Puppeteer v1.11.0](https://github.com/GoogleChrome/puppeteer/releases/tag/v1.11.0).
|
||||
|
||||
Example Dockerfile:
|
||||
|
||||
```Dockerfile
|
||||
FROM node:10-alpine
|
||||
|
||||
# Installs latest Chromium (72) package.
|
||||
RUN apk update && apk upgrade && \
|
||||
echo @edge http://nl.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories && \
|
||||
echo @edge http://nl.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories && \
|
||||
apk add --no-cache \
|
||||
chromium@edge=72.0.3626.121-r0 \
|
||||
nss@edge \
|
||||
freetype@edge \
|
||||
harfbuzz@edge \
|
||||
ttf-freefont@edge
|
||||
|
||||
...
|
||||
|
||||
# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
|
||||
# Puppeteer v1.11.0 works with Chromium 72.
|
||||
RUN yarn add puppeteer@1.11.0
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads /app \
|
||||
&& chown -R pptruser:pptruser /home/pptruser \
|
||||
&& chown -R pptruser:pptruser /app
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
And when launching Chrome, be sure to use the `chromium-browser` executable:
|
||||
|
||||
```js
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium-browser'
|
||||
});
|
||||
```
|
||||
|
||||
#### Tips
|
||||
|
||||
By default, Docker runs a container with a `/dev/shm` shared memory space 64MB.
|
||||
This is [typically too small](https://github.com/c0b/chrome-in-docker/issues/1) for Chrome
|
||||
and will cause Chrome to crash when rendering large pages. To fix, run the container with
|
||||
`docker run --shm-size=1gb` to increase the size of `/dev/shm`. Since Chrome 65, this is no
|
||||
longer necessary. Instead, launch the browser with the `--disable-dev-shm-usage` flag:
|
||||
|
||||
```js
|
||||
const browser = await puppeteer.launch({
|
||||
args: ['--disable-dev-shm-usage']
|
||||
});
|
||||
```
|
||||
|
||||
This will write shared memory files into `/tmp` instead of `/dev/shm`. See [crbug.com/736452](https://bugs.chromium.org/p/chromium/issues/detail?id=736452) for more details.
|
||||
|
||||
Seeing other weird errors when launching Chrome? Try running your container
|
||||
with `docker run --cap-add=SYS_ADMIN` when developing locally. Since the Dockerfile
|
||||
adds a `pptr` user as a non-privileged user, it may not have all the necessary privileges.
|
||||
|
||||
[dumb-init](https://github.com/Yelp/dumb-init) is worth checking out if you're
|
||||
experiencing a lot of zombies Chrome processes sticking around. There's special
|
||||
treatment for processes with PID=1, which makes it hard to terminate Chrome
|
||||
properly in some cases (e.g. in Docker).
|
||||
|
||||
## Running Puppeteer in the cloud
|
||||
|
||||
### Running Puppeteer on Google App Engine
|
||||
|
||||
The Node.js runtime of the [App Engine standard environment](https://cloud.google.com/appengine/docs/standard/nodejs/) comes with all system packages needed to run Headless Chrome.
|
||||
|
||||
To use `puppeteer`, simply list the module as a dependency in your `package.json` and deploy to Google App Engine. Read more about using `puppeteer` on App Engine by following [the official tutorial](https://cloud.google.com/appengine/docs/standard/nodejs/using-headless-chrome-with-puppeteer).
|
||||
|
||||
### Running Puppeteer on Google Cloud Functions
|
||||
|
||||
The Node.js 8 runtime of [Google Cloud Functions](https://cloud.google.com/functions/docs/) comes with all system packages needed to run Headless Chrome.
|
||||
|
||||
To use `puppeteer`, simply list the module as a dependency in your `package.json` and deploy your function to Google Cloud Functions using the `nodejs8` runtime.
|
||||
|
||||
### Running Puppeteer on Heroku
|
||||
|
||||
Running Puppeteer on Heroku requires some additional dependencies that aren't included on the Linux box that Heroku spins up for you. To add the dependencies on deploy, add the Puppeteer Heroku buildpack to the list of buildpacks for your app under Settings > Buildpacks.
|
||||
|
||||
The url for the buildpack is https://github.com/jontewks/puppeteer-heroku-buildpack
|
||||
|
||||
When you click add buildpack, simply paste that url into the input, and click save. On the next deploy, your app will also install the dependencies that Puppeteer needs to run.
|
||||
|
||||
If you need to render Chinese, Japanese, or Korean characters you may need to use a buildpack with additional font files like https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack
|
||||
|
||||
There's also another [simple guide](https://timleland.com/headless-chrome-on-heroku/) from @timleland that includes a sample project: https://timleland.com/headless-chrome-on-heroku/.
|
||||
|
||||
### Running Puppeteer on AWS Lambda
|
||||
|
||||
AWS Lambda [limits](https://docs.aws.amazon.com/lambda/latest/dg/limits.html) deployment package sizes to ~50MB. This presents challenges for running headless Chrome (and therefore Puppeteer) on Lambda. The community has put together a few resources that work around the issues:
|
||||
|
||||
- https://github.com/alixaxel/chrome-aws-lambda (kept updated with the latest stable release of puppeteer)
|
||||
- https://github.com/adieuadieu/serverless-chrome/blob/master/docs/chrome.md (serverless plugin - outdated)
|
||||
|
||||
## Code Transpilation Issues
|
||||
|
||||
If you are using a JavaScript transpiler like babel or TypeScript, calling `evaluate()` with an async function might not work. This is because while `puppeteer` uses `Function.prototype.toString()` to serialize functions while transpilers could be changing the output code in such a way it's incompatible with `puppeteer`.
|
||||
|
||||
Some workarounds to this problem would be to instruct the transpiler not to mess up with the code, for example, configure TypeScript to use latest ecma version (`"target": "es2018"`). Another workaround could be using string templates instead of functions:
|
||||
|
||||
```js
|
||||
await page.evaluate(`(async() => {
|
||||
console.log('1');
|
||||
})()`);
|
||||
```
|
|
@ -0,0 +1,37 @@
|
|||
# Running the examples
|
||||
|
||||
Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, the examples can be run from the root folder like so:
|
||||
|
||||
```sh
|
||||
NODE_PATH=../ node examples/search.js
|
||||
```
|
||||
|
||||
## Larger examples
|
||||
|
||||
More complex and use case driven examples can be found at [github.com/GoogleChromeLabs/puppeteer-examples](https://github.com/GoogleChromeLabs/puppeteer-examples).
|
||||
|
||||
# Other resources
|
||||
|
||||
> Other useful tools, articles, and projects that use Puppeteer.
|
||||
|
||||
## Rendering and web scraping
|
||||
|
||||
- [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron).
|
||||
- [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e "An article on medium") - Getting started with Puppeteer and Chrome Headless for Web Scraping.
|
||||
- [pupperender](https://github.com/LasaleFamine/pupperender) - Express middleware that checks the User-Agent header of incoming requests, and if it matches one of a configurable set of bots, render the page using Puppeteer. Useful for PWA rendering.
|
||||
- [headless-chrome-crawler](https://github.com/yujiosaka/headless-chrome-crawler) - Crawler that provides simple APIs to manipulate Headless Chrome and allows you to crawl dynamic websites.
|
||||
- [puppeteer-examples](https://github.com/checkly/puppeteer-examples) - Puppeteer Headless Chrome examples for real life use cases such as getting useful info from the web pages or common login scenarios.
|
||||
- [browserless](https://github.com/joelgriffith/browserless) - Headless Chrome as a service letting you execute Puppeteer scripts remotely. Provides a docker image with configuration for concurrency, launch arguments and more.
|
||||
- [Puppeteer Sandbox](https://puppeteersandbox.com) - Puppeteer sandbox environment as a service. Runs Puppeteer scripts and allows saving and embedding them in external sites and markdown files.
|
||||
|
||||
## Testing
|
||||
|
||||
- [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma.
|
||||
- [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome.
|
||||
- [puppeteer-to-istanbul-example](https://github.com/bcoe/puppeteer-to-istanbul-example) - Demo repository demonstrating how to output Puppeteer coverage in Istanbul format.
|
||||
- [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) - (almost) Zero configuration tool for setting up and running Jest and Puppeteer easily. Also includes an assertion library for Puppeteer.
|
||||
- [puppeteer-har](https://github.com/Everettss/puppeteer-har) - Generate HAR file with puppeteer.
|
||||
- [puppetry](https://puppetry.app/) - A desktop app to build Puppeteer/Jest driven tests without coding.
|
||||
|
||||
## Services
|
||||
- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps.
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Copyright 2017 Google Inc., PhantomJS Authors 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', request => {
|
||||
if (request.resourceType() === 'image')
|
||||
request.abort();
|
||||
else
|
||||
request.continue();
|
||||
});
|
||||
await page.goto('https://news.google.com/news/');
|
||||
await page.screenshot({path: 'news.png', fullPage: true});
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Define a window.onCustomEvent function on the page.
|
||||
await page.exposeFunction('onCustomEvent', e => {
|
||||
console.log(`${e.type} fired`, e.detail || '');
|
||||
});
|
||||
|
||||
/**
|
||||
* Attach an event listener to page to capture a custom event on page load/navigation.
|
||||
* @param {string} type Event name.
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function listenFor(type) {
|
||||
return page.evaluateOnNewDocument(type => {
|
||||
document.addEventListener(type, e => {
|
||||
window.onCustomEvent({type, detail: e.detail});
|
||||
});
|
||||
}, type);
|
||||
}
|
||||
|
||||
await listenFor('app-ready'); // Listen for "app-ready" custom event on page load.
|
||||
|
||||
await page.goto('https://www.chromestatus.com/features', {waitUntil: 'networkidle0'});
|
||||
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright 2017 Google Inc., PhantomJS Authors 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
function sniffDetector() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const platform = window.navigator.platform;
|
||||
|
||||
window.navigator.__defineGetter__('userAgent', function() {
|
||||
window.navigator.sniffed = true;
|
||||
return userAgent;
|
||||
});
|
||||
|
||||
window.navigator.__defineGetter__('platform', function() {
|
||||
window.navigator.sniffed = true;
|
||||
return platform;
|
||||
});
|
||||
}
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.evaluateOnNewDocument(sniffDetector);
|
||||
await page.goto('https://www.google.com', {waitUntil: 'networkidle2'});
|
||||
console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed)));
|
||||
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
|
||||
// page.pdf() is currently supported only in headless mode.
|
||||
// @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118
|
||||
await page.pdf({
|
||||
path: 'hn.pdf',
|
||||
format: 'letter'
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch({
|
||||
// Launch chromium using a proxy server on port 9876.
|
||||
// More on proxying:
|
||||
// https://www.chromium.org/developers/design-documents/network-settings
|
||||
args: [
|
||||
'--proxy-server=127.0.0.1:9876',
|
||||
// Use proxy for localhost URLs
|
||||
'--proxy-bypass-list=<-loopback>',
|
||||
]
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://google.com');
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const devices = require('puppeteer/DeviceDescriptors');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.emulate(devices['iPhone 6']);
|
||||
await page.goto('https://www.nytimes.com/');
|
||||
await page.screenshot({path: 'full.png', fullPage: true});
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://example.com');
|
||||
await page.screenshot({path: 'example.png'});
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Search developers.google.com/web for articles tagged
|
||||
* "Headless Chrome" and scrape results from the results page.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://developers.google.com/web/');
|
||||
|
||||
// Type into search box.
|
||||
await page.type('#searchbox input', 'Headless Chrome');
|
||||
|
||||
// Wait for suggest overlay to appear and click "show all results".
|
||||
const allResultsSelector = '.devsite-suggest-all-results';
|
||||
await page.waitForSelector(allResultsSelector);
|
||||
await page.click(allResultsSelector);
|
||||
|
||||
// Wait for the results page to load and display the results.
|
||||
const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
|
||||
await page.waitForSelector(resultsSelector);
|
||||
|
||||
// Extract the results from the page.
|
||||
const links = await page.evaluate(resultsSelector => {
|
||||
const anchors = Array.from(document.querySelectorAll(resultsSelector));
|
||||
return anchors.map(anchor => {
|
||||
const title = anchor.textContent.split('|')[0].trim();
|
||||
return `${title} - ${anchor.href}`;
|
||||
});
|
||||
}, resultsSelector);
|
||||
console.log(links.join('\n'));
|
||||
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:6.12.3
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
||||
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:8.11.3-stretch
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
|
||||
libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
|
||||
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add user so we don't need --no-sandbox.
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
|
@ -0,0 +1,11 @@
|
|||
FROM microsoft/windowsservercore:latest
|
||||
|
||||
ENV NODE_VERSION 8.11.3
|
||||
|
||||
RUN setx /m PATH "%PATH%;C:\nodejs"
|
||||
|
||||
RUN powershell -Command \
|
||||
netsh interface ipv4 set subinterface 18 mtu=1460 store=persistent ; \
|
||||
Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \
|
||||
Expand-Archive node.zip -DestinationPath C:\ ; \
|
||||
Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs'
|
|
@ -0,0 +1,31 @@
|
|||
env:
|
||||
DISPLAY: :99.0
|
||||
|
||||
task:
|
||||
name: node8 (linux)
|
||||
container:
|
||||
dockerfile: .ci/node8/Dockerfile.linux
|
||||
xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
|
||||
install_script: npm install
|
||||
test_script: npm run funit
|
||||
|
||||
task:
|
||||
name: node8 (macOS)
|
||||
osx_instance:
|
||||
image: high-sierra-base
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
node_install_script:
|
||||
- brew install node@8
|
||||
- brew link --force node@8
|
||||
install_script: npm install
|
||||
test_script: npm run funit
|
||||
|
||||
# task:
|
||||
# allow_failures: true
|
||||
# windows_container:
|
||||
# dockerfile: .ci/node8/Dockerfile.windows
|
||||
# os_version: 2016
|
||||
# name: node8 (windows)
|
||||
# install_script: npm install --unsafe-perm
|
||||
# test_script: npm run funit
|
|
@ -0,0 +1,10 @@
|
|||
/node_modules/
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.local-browser
|
||||
/test/output-chromium
|
||||
/test/output-firefox
|
|
@ -0,0 +1,37 @@
|
|||
# exclude all tests
|
||||
test
|
||||
utils/node6-transform
|
||||
|
||||
# exclude internal type definition files
|
||||
/lib/*.d.ts
|
||||
/node6/lib/*.d.ts
|
||||
|
||||
# repeats from .gitignore
|
||||
node_modules
|
||||
.local-chromium
|
||||
.local-browser
|
||||
.dev_profile*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.pyc
|
||||
.vscode
|
||||
package-lock.json
|
||||
/node6/test
|
||||
/node6/utils
|
||||
/test
|
||||
/utils
|
||||
/docs
|
||||
yarn.lock
|
||||
|
||||
# other
|
||||
/.ci
|
||||
/examples
|
||||
.appveyour.yml
|
||||
.cirrus.yml
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.travis.yml
|
||||
README.md
|
||||
tsconfig.json
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let asyncawait = true;
|
||||
try {
|
||||
new Function('async function test(){await 1}');
|
||||
} catch (error) {
|
||||
asyncawait = false;
|
||||
}
|
||||
|
||||
// If node does not support async await, use the compiled version.
|
||||
if (asyncawait)
|
||||
module.exports = require('./lib/DeviceDescriptors');
|
||||
else
|
||||
module.exports = require('./node6/lib/DeviceDescriptors');
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('./lib/Errors');
|
|
@ -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.
|
|
@ -0,0 +1,56 @@
|
|||
<img src="https://user-images.githubusercontent.com/39191/49555713-a07b3c00-f8b5-11e8-8aba-f2d03cd83da5.png" height="200" align="right">
|
||||
|
||||
# Prototype: Puppeteer for Firefox
|
||||
|
||||
> Use Puppeteer's API with Firefox
|
||||
|
||||
**⚠️ BEWARE**: Experimental. Just for preview. Installation and usage will change.
|
||||
|
||||
This project is a feasibility prototype to guide the work of implementing Puppeteer endpoints into Firefox's code base. Mozilla's [bug 1545057](https://bugzilla.mozilla.org/show_bug.cgi?id=1545057) tracks the initial milestone, which will be based on a CDP-based [remote protocol](https://wiki.mozilla.org/Remote).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
To try out Puppeteer with Firefox in your project, run:
|
||||
|
||||
```bash
|
||||
npm i puppeteer-firefox
|
||||
# or "yarn add puppeteer-firefox"
|
||||
```
|
||||
|
||||
Note: When you install puppeteer-firefox, it downloads a [custom-built Firefox](https://github.com/puppeteer/juggler) (Firefox/63.0.4) that is guaranteed to work with the API.
|
||||
|
||||
### Usage
|
||||
|
||||
**Example** - navigating to https://example.com and saving a screenshot as *example.png*:
|
||||
|
||||
Save file as **example.js**
|
||||
|
||||
```js
|
||||
const pptrFirefox = require('puppeteer-firefox');
|
||||
|
||||
(async () => {
|
||||
const browser = await pptrFirefox.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
|
||||
```
|
||||
|
||||
|
||||
### API Status
|
||||
|
||||
Current tip-of-tree status of Puppeteer-Firefox is availabe at [isPuppeteerFirefoxReady?](https://aslushnikov.github.io/ispuppeteerfirefoxready/)
|
||||
|
||||
|
||||
### Credits
|
||||
|
||||
Special thanks to [Amine Bouhlali](https://bitbucket.org/aminerop/) who volunteered the [`puppeteer-firefox`](https://www.npmjs.com/package/puppeteer-firefox) NPM package.
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer-firefox');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://example.com');
|
||||
await page.screenshot({path: 'example.png'});
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Search developers.google.com/web for articles tagged
|
||||
* "Headless Chrome" and scrape results from the results page.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const puppeteer = require('puppeteer-firefox');
|
||||
|
||||
(async() => {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://developers.google.com/web/');
|
||||
|
||||
// Type into search box.
|
||||
await page.type('#searchbox input', 'Headless Chrome');
|
||||
|
||||
// Wait for suggest overlay to appear and click "show all results".
|
||||
const allResultsSelector = '.devsite-suggest-all-results';
|
||||
await page.waitForSelector(allResultsSelector);
|
||||
await page.click(allResultsSelector);
|
||||
|
||||
// Wait for the results page to load and display the results.
|
||||
const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
|
||||
await page.waitForSelector(resultsSelector);
|
||||
|
||||
// Extract the results from the page.
|
||||
const links = await page.evaluate(resultsSelector => {
|
||||
const anchors = Array.from(document.querySelectorAll(resultsSelector));
|
||||
return anchors.map(anchor => {
|
||||
const title = anchor.textContent.split('|')[0].trim();
|
||||
return `${title} - ${anchor.href}`;
|
||||
});
|
||||
}, resultsSelector);
|
||||
console.log(links.join('\n'));
|
||||
|
||||
await browser.close();
|
||||
})();
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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} = require('./lib/helper');
|
||||
const api = require('./lib/api');
|
||||
for (const className in api)
|
||||
helper.installAsyncStackHooks(api[className]);
|
||||
|
||||
const {Puppeteer} = require('./lib/Puppeteer');
|
||||
const packageJson = require('./package.json');
|
||||
const preferredRevision = packageJson.puppeteer.firefox_revision;
|
||||
module.exports = new Puppeteer(__dirname, preferredRevision);
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
// puppeteer-core should not install anything.
|
||||
if (require('./package.json').name === 'puppeteer-core')
|
||||
return;
|
||||
|
||||
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 browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost, product: 'firefox' });
|
||||
|
||||
const revision = require('./package.json').puppeteer.firefox_revision;
|
||||
|
||||
const revisionInfo = browserFetcher.revisionInfo(revision);
|
||||
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local)
|
||||
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;
|
||||
|
||||
browserFetcher.download(revisionInfo.revision, onProgress)
|
||||
.then(() => browserFetcher.localRevisions())
|
||||
.then(onSuccess)
|
||||
.catch(onError);
|
||||
|
||||
/**
|
||||
* @param {!Array<string>}
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function onSuccess(localRevisions) {
|
||||
console.log('Firefox downloaded to ' + revisionInfo.folderPath);
|
||||
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
|
||||
// Remove previous firefox revisions.
|
||||
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
|
||||
const installFirefoxPreferences = require('./misc/install-preferences');
|
||||
return Promise.all([...cleanupOldVersions, installFirefoxPreferences(revisionInfo.executablePath)]).then(() => {
|
||||
console.log('Firefox preferences installed!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
function onError(error) {
|
||||
console.error(`ERROR: Failed to download Firefox r${revision}!`);
|
||||
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 Firefox+Puppeteer ${revision.substring(0, 8)} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
||||
complete: '|',
|
||||
incomplete: ' ',
|
||||
width: 20,
|
||||
total: totalBytes,
|
||||
});
|
||||
}
|
||||
const delta = downloadedBytes - lastDownloadedBytes;
|
||||
lastDownloadedBytes = downloadedBytes;
|
||||
progressBar.tick(delta);
|
||||
}
|
||||
|
||||
function toMegabytes(bytes) {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return `${Math.round(mb * 10) / 10} Mb`;
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
/**
|
||||
* @typedef {Object} SerializedAXNode
|
||||
* @property {string} role
|
||||
*
|
||||
* @property {string=} name
|
||||
* @property {string|number=} value
|
||||
* @property {string=} description
|
||||
*
|
||||
* @property {string=} keyshortcuts
|
||||
* @property {string=} roledescription
|
||||
* @property {string=} valuetext
|
||||
*
|
||||
* @property {boolean=} disabled
|
||||
* @property {boolean=} expanded
|
||||
* @property {boolean=} focused
|
||||
* @property {boolean=} modal
|
||||
* @property {boolean=} multiline
|
||||
* @property {boolean=} multiselectable
|
||||
* @property {boolean=} readonly
|
||||
* @property {boolean=} required
|
||||
* @property {boolean=} selected
|
||||
*
|
||||
* @property {boolean|"mixed"=} checked
|
||||
* @property {boolean|"mixed"=} pressed
|
||||
*
|
||||
* @property {number=} level
|
||||
*
|
||||
* @property {string=} autocomplete
|
||||
* @property {string=} haspopup
|
||||
* @property {string=} invalid
|
||||
* @property {string=} orientation
|
||||
*
|
||||
* @property {Array<SerializedAXNode>=} children
|
||||
*/
|
||||
|
||||
class Accessibility {
|
||||
constructor(session) {
|
||||
this._session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{interestingOnly?: boolean}=} options
|
||||
* @return {!Promise<!SerializedAXNode>}
|
||||
*/
|
||||
async snapshot(options = {}) {
|
||||
const {interestingOnly = true} = options;
|
||||
const {tree} = await this._session.send('Accessibility.getFullAXTree');
|
||||
const root = new AXNode(tree);
|
||||
if (!interestingOnly)
|
||||
return serializeTree(root)[0];
|
||||
|
||||
/** @type {!Set<!AXNode>} */
|
||||
const interestingNodes = new Set();
|
||||
collectInterestingNodes(interestingNodes, root, false);
|
||||
return serializeTree(root, interestingNodes)[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Set<!AXNode>} collection
|
||||
* @param {!AXNode} node
|
||||
* @param {boolean} insideControl
|
||||
*/
|
||||
function 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)
|
||||
collectInterestingNodes(collection, child, insideControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!AXNode} node
|
||||
* @param {!Set<!AXNode>=} whitelistedNodes
|
||||
* @return {!Array<!SerializedAXNode>}
|
||||
*/
|
||||
function serializeTree(node, whitelistedNodes) {
|
||||
/** @type {!Array<!SerializedAXNode>} */
|
||||
const children = [];
|
||||
for (const child of node._children)
|
||||
children.push(...serializeTree(child, whitelistedNodes));
|
||||
|
||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
||||
return children;
|
||||
|
||||
const serializedNode = node.serialize();
|
||||
if (children.length)
|
||||
serializedNode.children = children;
|
||||
return [serializedNode];
|
||||
}
|
||||
|
||||
|
||||
class AXNode {
|
||||
constructor(payload) {
|
||||
this._payload = payload;
|
||||
|
||||
/** @type {!Array<!AXNode>} */
|
||||
this._children = (payload.children || []).map(x => new AXNode(x));
|
||||
|
||||
this._editable = payload.editable;
|
||||
this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input');
|
||||
this._focusable = payload.focusable;
|
||||
this._expanded = payload.expanded;
|
||||
this._name = this._payload.name;
|
||||
this._role = this._payload.role;
|
||||
this._cachedHasFocusableChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isPlainTextField() {
|
||||
if (this._richlyEditable)
|
||||
return false;
|
||||
if (this._editable)
|
||||
return true;
|
||||
return this._role === 'entry';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isTextOnlyObject() {
|
||||
const role = this._role;
|
||||
return (role === 'text leaf' || role === 'text' || role === 'statictext');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
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 'graphic':
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isControl() {
|
||||
switch (this._role) {
|
||||
case 'checkbutton':
|
||||
case 'check menu item':
|
||||
case 'check rich option':
|
||||
case 'combobox':
|
||||
case 'combobox option':
|
||||
case 'color chooser':
|
||||
case 'listbox':
|
||||
case 'listbox option':
|
||||
case 'listbox rich option':
|
||||
case 'popup menu':
|
||||
case 'menupopup':
|
||||
case 'menuitem':
|
||||
case 'menubar':
|
||||
case 'button':
|
||||
case 'pushbutton':
|
||||
case 'radiobutton':
|
||||
case 'radio menuitem':
|
||||
case 'scrollbar':
|
||||
case 'slider':
|
||||
case 'spinbutton':
|
||||
case 'switch':
|
||||
case 'pagetab':
|
||||
case 'entry':
|
||||
case 'tree table':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} insideControl
|
||||
* @return {boolean}
|
||||
*/
|
||||
isInteresting(insideControl) {
|
||||
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.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SerializedAXNode}
|
||||
*/
|
||||
serialize() {
|
||||
/** @type {SerializedAXNode} */
|
||||
const node = {
|
||||
role: this._role
|
||||
};
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const userStringProperties = [
|
||||
'name',
|
||||
'value',
|
||||
'description',
|
||||
'roledescription',
|
||||
'valuetext',
|
||||
'keyshortcuts',
|
||||
];
|
||||
for (const userStringProperty of userStringProperties) {
|
||||
if (!(userStringProperty in this._payload))
|
||||
continue;
|
||||
node[userStringProperty] = this._payload[userStringProperty];
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const booleanProperties = [
|
||||
'disabled',
|
||||
'expanded',
|
||||
'focused',
|
||||
'modal',
|
||||
'multiline',
|
||||
'multiselectable',
|
||||
'readonly',
|
||||
'required',
|
||||
'selected',
|
||||
];
|
||||
for (const booleanProperty of booleanProperties) {
|
||||
if (this._role === 'document' && booleanProperty === 'focused')
|
||||
continue; // document focusing is strange
|
||||
const value = this._payload[booleanProperty];
|
||||
if (!value)
|
||||
continue;
|
||||
node[booleanProperty] = value;
|
||||
}
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const tristateProperties = [
|
||||
'checked',
|
||||
'pressed',
|
||||
];
|
||||
for (const tristateProperty of tristateProperties) {
|
||||
if (!(tristateProperty in this._payload))
|
||||
continue;
|
||||
const value = this._payload[tristateProperty];
|
||||
node[tristateProperty] = value;
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const numericalProperties = [
|
||||
'level',
|
||||
'valuemax',
|
||||
'valuemin',
|
||||
];
|
||||
for (const numericalProperty of numericalProperties) {
|
||||
if (!(numericalProperty in this._payload))
|
||||
continue;
|
||||
node[numericalProperty] = this._payload[numericalProperty];
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const tokenProperties = [
|
||||
'autocomplete',
|
||||
'haspopup',
|
||||
'invalid',
|
||||
'orientation',
|
||||
];
|
||||
for (const tokenProperty of tokenProperties) {
|
||||
const value = this._payload[tokenProperty];
|
||||
if (!value || value === 'false')
|
||||
continue;
|
||||
node[tokenProperty] = value;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Accessibility};
|
|
@ -0,0 +1,369 @@
|
|||
const {helper, assert} = require('./helper');
|
||||
const {Page} = require('./Page');
|
||||
const {Events} = require('./Events');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Browser extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {function():void} closeCallback
|
||||
*/
|
||||
static async create(connection, defaultViewport, process, closeCallback) {
|
||||
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
|
||||
const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback);
|
||||
await connection.send('Target.enable');
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Array<string>} browserContextIds
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {function():void} closeCallback
|
||||
*/
|
||||
constructor(connection, browserContextIds, defaultViewport, process, closeCallback) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._process = process;
|
||||
this._closeCallback = closeCallback;
|
||||
|
||||
/** @type {!Map<string, !Target>} */
|
||||
this._targets = new Map();
|
||||
|
||||
this._defaultContext = new BrowserContext(this._connection, this, null);
|
||||
/** @type {!Map<string, !BrowserContext>} */
|
||||
this._contexts = new Map();
|
||||
for (const browserContextId of browserContextIds)
|
||||
this._contexts.set(browserContextId, new BrowserContext(this._connection, this, browserContextId));
|
||||
|
||||
this._connection.on(Events.Connection.Disconnected, () => this.emit(Events.Browser.Disconnected));
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)),
|
||||
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
|
||||
helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
wsEndpoint() {
|
||||
return this._connection.url();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._connection.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return !this._connection._closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!BrowserContext}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!BrowserContext>}
|
||||
*/
|
||||
browserContexts() {
|
||||
return [this._defaultContext, ...Array.from(this._contexts.values())];
|
||||
}
|
||||
|
||||
defaultBrowserContext() {
|
||||
return this._defaultContext;
|
||||
}
|
||||
|
||||
async _disposeContext(browserContextId) {
|
||||
await this._connection.send('Target.removeBrowserContext', {browserContextId});
|
||||
this._contexts.delete(browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async userAgent() {
|
||||
const info = await this._connection.send('Browser.getInfo');
|
||||
return info.userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async version() {
|
||||
const info = await this._connection.send('Browser.getInfo');
|
||||
return info.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.ChildProcess}
|
||||
*/
|
||||
process() {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.Browser.TargetCreated, check);
|
||||
this.on('targetchanged', check);
|
||||
try {
|
||||
if (!timeout)
|
||||
return await targetPromise;
|
||||
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
|
||||
} finally {
|
||||
this.removeListener(Events.Browser.TargetCreated, check);
|
||||
this.removeListener('targetchanged', check);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Target} target
|
||||
*/
|
||||
function check(target) {
|
||||
if (predicate(target))
|
||||
resolve(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<Page>}
|
||||
*/
|
||||
newPage() {
|
||||
return this._createPageInContext(this._defaultContext._browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} browserContextId
|
||||
* @return {Promise<Page>}
|
||||
*/
|
||||
async _createPageInContext(browserContextId) {
|
||||
const {targetId} = await this._connection.send('Target.newPage', {
|
||||
browserContextId: browserContextId || undefined
|
||||
});
|
||||
const target = this._targets.get(targetId);
|
||||
return await target.page();
|
||||
}
|
||||
|
||||
async pages() {
|
||||
const pageTargets = Array.from(this._targets.values()).filter(target => target.type() === 'page');
|
||||
return await Promise.all(pageTargets.map(target => target.page()));
|
||||
}
|
||||
|
||||
targets() {
|
||||
return Array.from(this._targets.values());
|
||||
}
|
||||
|
||||
target() {
|
||||
return this.targets().find(target => target.type() === 'browser');
|
||||
}
|
||||
|
||||
async _onTargetCreated({targetId, url, browserContextId, openerId, type}) {
|
||||
const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext;
|
||||
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
|
||||
this._targets.set(targetId, target);
|
||||
if (target.opener() && target.opener()._pagePromise) {
|
||||
const openerPage = await target.opener()._pagePromise;
|
||||
if (openerPage.listenerCount(Events.Page.Popup)) {
|
||||
const popupPage = await target.page();
|
||||
openerPage.emit(Events.Page.Popup, popupPage);
|
||||
}
|
||||
}
|
||||
this.emit(Events.Browser.TargetCreated, target);
|
||||
context.emit(Events.BrowserContext.TargetCreated, target);
|
||||
}
|
||||
|
||||
_onTargetDestroyed({targetId}) {
|
||||
const target = this._targets.get(targetId);
|
||||
this._targets.delete(targetId);
|
||||
target._closedCallback();
|
||||
this.emit(Events.Browser.TargetDestroyed, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target);
|
||||
}
|
||||
|
||||
_onTargetInfoChanged({targetId, url}) {
|
||||
const target = this._targets.get(targetId);
|
||||
target._url = url;
|
||||
this.emit(Events.Browser.TargetChanged, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
|
||||
}
|
||||
|
||||
async close() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
await this._closeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
class Target {
|
||||
/**
|
||||
*
|
||||
* @param {*} connection
|
||||
* @param {!Browser} browser
|
||||
* @param {!BrowserContext} context
|
||||
* @param {string} targetId
|
||||
* @param {string} type
|
||||
* @param {string} url
|
||||
* @param {string=} openerId
|
||||
*/
|
||||
constructor(connection, browser, context, targetId, type, url, openerId) {
|
||||
this._browser = browser;
|
||||
this._context = context;
|
||||
this._connection = connection;
|
||||
this._targetId = targetId;
|
||||
this._type = type;
|
||||
/** @type {?Promise<!Page>} */
|
||||
this._pagePromise = null;
|
||||
this._url = url;
|
||||
this._openerId = openerId;
|
||||
this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Target}
|
||||
*/
|
||||
opener() {
|
||||
return this._openerId ? this._browser._targets.get(this._openerId) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {"page"|"browser"}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!BrowserContext}
|
||||
*/
|
||||
browserContext() {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
async page() {
|
||||
if (this._type === 'page' && !this._pagePromise) {
|
||||
const session = await this._connection.createSession(this._targetId);
|
||||
this._pagePromise = Page.create(session, this, this._browser._defaultViewport);
|
||||
}
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserContext extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Browser} browser
|
||||
* @param {?string} browserContextId
|
||||
*/
|
||||
constructor(connection, browser, browserContextId) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._browser = browser;
|
||||
this._browserContextId = browserContextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} origin
|
||||
* @param {!Array<string>} permissions
|
||||
*/
|
||||
async overridePermissions(origin, permissions) {
|
||||
const webPermissionToProtocol = new Map([
|
||||
['geolocation', 'geo'],
|
||||
['microphone', 'microphone'],
|
||||
['camera', 'camera'],
|
||||
['notifications', 'desktop-notifications'],
|
||||
]);
|
||||
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._browserContextId || undefined, permissions});
|
||||
}
|
||||
|
||||
async clearPermissionOverrides() {
|
||||
await this._connection.send('Browser.resetPermissions', {browserContextId: this._browserContextId || undefined});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<Target>}
|
||||
*/
|
||||
targets() {
|
||||
return this._browser.targets().filter(target => target.browserContext() === this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<Array<Puppeteer.Page>>}
|
||||
*/
|
||||
async pages() {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => target.type() === 'page')
|
||||
.map(target => target.page())
|
||||
);
|
||||
return pages.filter(page => !!page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(Target):boolean} predicate
|
||||
* @param {{timeout?: number}=} options
|
||||
* @return {!Promise<Target>}
|
||||
*/
|
||||
waitForTarget(predicate, options) {
|
||||
return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isIncognito() {
|
||||
return !!this._browserContextId;
|
||||
}
|
||||
|
||||
newPage() {
|
||||
return this._browser._createPageInContext(this._browserContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Browser}
|
||||
*/
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
async close() {
|
||||
assert(this._browserContextId, 'Non-incognito contexts cannot be closed!');
|
||||
await this._browser._disposeContext(this._browserContextId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Browser, BrowserContext, Target};
|
|
@ -0,0 +1,342 @@
|
|||
/**
|
||||
* 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 fs = require('fs');
|
||||
const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
const util = require('util');
|
||||
const URL = require('url');
|
||||
const {helper, assert} = require('./helper');
|
||||
const removeRecursive = require('rimraf');
|
||||
// @ts-ignore
|
||||
const ProxyAgent = require('https-proxy-agent');
|
||||
// @ts-ignore
|
||||
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
|
||||
|
||||
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
|
||||
|
||||
const downloadURLs = {
|
||||
chromium: {
|
||||
linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip',
|
||||
mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip',
|
||||
win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip',
|
||||
win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip',
|
||||
},
|
||||
firefox: {
|
||||
linux: '%s/juggler-builds/%s/%s.zip',
|
||||
mac: '%s/juggler-builds/%s/%s.zip',
|
||||
win32: '%s/juggler-builds/%s/%s.zip',
|
||||
win64: '%s/juggler-builds/%s/%s.zip',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} product
|
||||
* @param {string} platform
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function archiveName(product, platform, revision) {
|
||||
if (product === 'chromium') {
|
||||
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') {
|
||||
if (platform === 'linux')
|
||||
return 'firefox-linux';
|
||||
if (platform === 'mac')
|
||||
return 'firefox-mac';
|
||||
if (platform === 'win32' || platform === 'win64')
|
||||
return 'firefox-' + platform;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} product
|
||||
* @param {string} platform
|
||||
* @param {string} host
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function downloadURL(product, platform, host, revision) {
|
||||
return util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision));
|
||||
}
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
||||
|
||||
function existsAsync(filePath) {
|
||||
let fulfill = null;
|
||||
const promise = new Promise(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
|
||||
class BrowserFetcher {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {!BrowserFetcher.Options=} options
|
||||
*/
|
||||
constructor(projectRoot, options = {}) {
|
||||
this._product = (options.product || 'chromium').toLowerCase();
|
||||
assert(this._product === 'chromium' || this._product === 'firefox', `Unkown product: "${options.product}"`);
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser');
|
||||
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
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';
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!Promise<boolean>}
|
||||
*/
|
||||
canDownload(revision) {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
let resolve;
|
||||
const promise = new Promise(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', error => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @param {?function(number, number)} progressCallback
|
||||
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
|
||||
*/
|
||||
async download(revision, progressCallback) {
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`);
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
if (await existsAsync(folderPath))
|
||||
return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
try {
|
||||
await downloadFile(url, zipPath, progressCallback);
|
||||
await extractZip(zipPath, folderPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const revisionInfo = this.revisionInfo(revision);
|
||||
if (revisionInfo)
|
||||
await chmodAsync(revisionInfo.executablePath, 0o755);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
async localRevisions() {
|
||||
if (!await existsAsync(this._downloadsFolder))
|
||||
return [];
|
||||
const fileNames = await readdirAsync(this._downloadsFolder);
|
||||
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
*/
|
||||
async remove(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
|
||||
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!BrowserFetcher.RevisionInfo}
|
||||
*/
|
||||
revisionInfo(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._product === 'chromium') {
|
||||
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);
|
||||
}
|
||||
const url = downloadURL(this._product, this._platform, this._downloadHost, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
_getFolderPath(revision) {
|
||||
return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {BrowserFetcher};
|
||||
|
||||
/**
|
||||
* @param {string} folderPath
|
||||
* @return {?{platform: string, revision: string}}
|
||||
*/
|
||||
function parseFolderPath(folderPath) {
|
||||
const name = path.basename(folderPath);
|
||||
const splits = name.split('-');
|
||||
if (splits.length !== 3)
|
||||
return null;
|
||||
const [product, platform, revision] = splits;
|
||||
if (!downloadURLs[product][platform])
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} destinationPath
|
||||
* @param {?function(number, number)} progressCallback
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function downloadFile(url, destinationPath, progressCallback) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} zipPath
|
||||
* @param {string} folderPath
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
function extractZip(zipPath, folderPath) {
|
||||
return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
fulfill();
|
||||
}));
|
||||
}
|
||||
|
||||
function httpRequest(url, method, response) {
|
||||
/** @type {Object} */
|
||||
const options = URL.parse(url);
|
||||
options.method = method;
|
||||
|
||||
const proxyURL = getProxyForUrl(url);
|
||||
if (proxyURL) {
|
||||
/** @type {Object} */
|
||||
const parsedProxyURL = URL.parse(proxyURL);
|
||||
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
||||
|
||||
options.agent = new ProxyAgent(parsedProxyURL);
|
||||
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:' ?
|
||||
require('https').request(options, requestCallback) :
|
||||
require('http').request(options, requestCallback);
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.Options
|
||||
* @property {string=} platform
|
||||
* @property {string=} path
|
||||
* @property {string=} host
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.RevisionInfo
|
||||
* @property {string} folderPath
|
||||
* @property {string} executablePath
|
||||
* @property {string} url
|
||||
* @property {boolean} local
|
||||
* @property {string} revision
|
||||
*/
|
|
@ -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.
|
||||
*/
|
||||
const {assert} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
const debugProtocol = require('debug')('puppeteer:protocol');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!Puppeteer.ConnectionTransport} transport
|
||||
* @param {number=} delay
|
||||
*/
|
||||
constructor(url, transport, delay = 0) {
|
||||
super();
|
||||
this._url = url;
|
||||
this._lastId = 0;
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._delay = delay;
|
||||
|
||||
this._transport = transport;
|
||||
this._transport.onmessage = this._onMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
/** @type {!Map<string, !JugglerSession>}*/
|
||||
this._sessions = new Map();
|
||||
this._closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!JugglerSession} session
|
||||
* @return {!Connection}
|
||||
*/
|
||||
static fromSession(session) {
|
||||
return session._connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionId
|
||||
* @return {?JugglerSession}
|
||||
*/
|
||||
session(sessionId) {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
send(method, params = {}) {
|
||||
const id = this._rawSend({method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} message
|
||||
* @return {number}
|
||||
*/
|
||||
_rawSend(message) {
|
||||
const id = ++this._lastId;
|
||||
message = JSON.stringify(Object.assign({}, message, {id}));
|
||||
debugProtocol('SEND ► ' + message);
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
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 JugglerSession(this, object.params.targetInfo.type, sessionId);
|
||||
this._sessions.set(sessionId, session);
|
||||
} else if (object.method === 'Browser.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.Connection.Disconnected);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onClose();
|
||||
this._transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} targetId
|
||||
* @return {!Promise<!JugglerSession>}
|
||||
*/
|
||||
async createSession(targetId) {
|
||||
const {sessionId} = await this.send('Target.attachToTarget', {targetId});
|
||||
return this._sessions.get(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
class JugglerSession extends EventEmitter {
|
||||
/**
|
||||
* @param {!Connection} connection
|
||||
* @param {string} targetType
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
constructor(connection, targetType, sessionId) {
|
||||
super();
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._connection = connection;
|
||||
this._targetType = targetType;
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
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, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
|
||||
*/
|
||||
_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 {
|
||||
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.JugglerSession.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
module.exports = {Connection, JugglerSession};
|
|
@ -0,0 +1,625 @@
|
|||
const {helper, assert} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
class DOMWorld {
|
||||
constructor(frame, timeoutSettings) {
|
||||
this._frame = frame;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
|
||||
this._documentPromise = null;
|
||||
this._contextPromise;
|
||||
this._contextResolveCallback = null;
|
||||
this._setContext(null);
|
||||
|
||||
/** @type {!Set<!WaitTask>} */
|
||||
this._waitTasks = new Set();
|
||||
this._detached = false;
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
_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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_detach() {
|
||||
this._detached = true;
|
||||
for (const waitTask of this._waitTasks)
|
||||
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||
}
|
||||
|
||||
async executionContext() {
|
||||
if (this._detached)
|
||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||
return this._contextPromise;
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
const context = await this.executionContext();
|
||||
return context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
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();
|
||||
return document.$(selector);
|
||||
}
|
||||
|
||||
_document() {
|
||||
if (!this._documentPromise)
|
||||
this._documentPromise = this.evaluateHandle('document').then(handle => handle.asElement());
|
||||
return this._documentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const document = await this._document();
|
||||
return document.$x(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
const document = await this._document();
|
||||
return document.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const document = await this._document();
|
||||
return document.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const document = await this._document();
|
||||
return document.$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
await this.evaluate(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
}, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
if (typeof options.url === 'string') {
|
||||
const url = options.url;
|
||||
try {
|
||||
return (await this.evaluateHandle(addScriptUrl, url, options.type)).asElement();
|
||||
} catch (error) {
|
||||
throw new Error(`Loading script from ${url} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.path === 'string') {
|
||||
let contents = await readFileAsync(options.path, 'utf8');
|
||||
contents += '//# sourceURL=' + options.path.replace(/\n/g, '');
|
||||
return (await this.evaluateHandle(addScriptContent, contents, options.type)).asElement();
|
||||
}
|
||||
|
||||
if (typeof options.content === 'string') {
|
||||
return (await this.evaluateHandle(addScriptContent, options.content, options.type)).asElement();
|
||||
}
|
||||
|
||||
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} type
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} type
|
||||
* @return {!HTMLElement}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
if (typeof options.url === 'string') {
|
||||
const url = options.url;
|
||||
try {
|
||||
return (await this.evaluateHandle(addStyleUrl, url)).asElement();
|
||||
} catch (error) {
|
||||
throw new Error(`Loading style from ${url} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.path === 'string') {
|
||||
let contents = await readFileAsync(options.path, 'utf8');
|
||||
contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/';
|
||||
return (await this.evaluateHandle(addStyleContent, contents)).asElement();
|
||||
}
|
||||
|
||||
if (typeof options.content === 'string') {
|
||||
return (await this.evaluateHandle(addStyleContent, options.content)).asElement();
|
||||
}
|
||||
|
||||
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.click(options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.focus();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.hover();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values) {
|
||||
for (const value of values)
|
||||
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
return this.$eval(selector, (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.tap();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.type(text, options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForSelector(selector, options) {
|
||||
return this._waitForSelectorOrXPath(selector, false, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForXPath(xpath, options) {
|
||||
return this._waitForSelectorOrXPath(xpath, true, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this.evaluate(() => document.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selectorOrXPath
|
||||
* @param {boolean} isXPath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
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 waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, 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
|
||||
: document.querySelector(selectorOrXPath);
|
||||
if (!node)
|
||||
return waitForHidden;
|
||||
if (!waitForVisible && !waitForHidden)
|
||||
return node;
|
||||
const element = /** @type {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;
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
function hasVisibleBoundingBox() {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class WaitTask {
|
||||
/**
|
||||
* @param {!DOMWorld} domWorld
|
||||
* @param {Function|string} predicateBody
|
||||
* @param {string|number} polling
|
||||
* @param {number} timeout
|
||||
* @param {!Array<*>} args
|
||||
*/
|
||||
constructor(domWorld, predicateBody, title, polling, timeout, ...args) {
|
||||
if (helper.isString(polling))
|
||||
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
||||
else if (helper.isNumber(polling))
|
||||
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
||||
else
|
||||
throw new Error('Unknown polling options: ' + polling);
|
||||
|
||||
this._domWorld = domWorld;
|
||||
this._polling = polling;
|
||||
this._timeout = timeout;
|
||||
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
||||
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 TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
|
||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
||||
}
|
||||
this.rerun();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
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 this._domWorld.evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
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(e => true)) {
|
||||
await success.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// When the page is navigated, the promise is rejected.
|
||||
// Try again right away.
|
||||
if (error && error.message.includes('Execution context was destroyed')) {
|
||||
this.rerun();
|
||||
return;
|
||||
}
|
||||
|
||||
if (error)
|
||||
this._reject(error);
|
||||
else
|
||||
this._resolve(success);
|
||||
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._domWorld._waitTasks.delete(this);
|
||||
this._runningTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} predicateBody
|
||||
* @param {string} polling
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
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<*>}
|
||||
*/
|
||||
function pollMutation() {
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
return Promise.resolve(success);
|
||||
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
const observer = new MutationObserver(mutations => {
|
||||
if (timedOut) {
|
||||
observer.disconnect();
|
||||
fulfill();
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success) {
|
||||
observer.disconnect();
|
||||
fulfill(success);
|
||||
}
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollRaf() {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onRaf();
|
||||
return result;
|
||||
|
||||
function onRaf() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
requestAnimationFrame(onRaf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pollInterval
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollInterval(pollInterval) {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onTimeout();
|
||||
return result;
|
||||
|
||||
function onTimeout() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
setTimeout(onTimeout, pollInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {DOMWorld};
|
|
@ -0,0 +1,824 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
'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/73.0.3679.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/73.0.3679.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': '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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.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/73.0.3679.0 Mobile Safari/537.36',
|
||||
'viewport': {
|
||||
'width': 823,
|
||||
'height': 411,
|
||||
'deviceScaleFactor': 3.5,
|
||||
'isMobile': true,
|
||||
'hasTouch': true,
|
||||
'isLandscape': true
|
||||
}
|
||||
}
|
||||
];
|
||||
for (const device of module.exports)
|
||||
module.exports[device.name] = device;
|
|
@ -0,0 +1,57 @@
|
|||
const {helper, assert, debugError} = require('./helper');
|
||||
|
||||
class Dialog {
|
||||
constructor(client, payload) {
|
||||
this._client = client;
|
||||
this._dialogId = payload.dialogId;
|
||||
this._type = payload.type;
|
||||
this._message = payload.message;
|
||||
this._handled = false;
|
||||
this._defaultValue = payload.defaultValue || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
message() {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
defaultValue() {
|
||||
return this._defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string=} promptText
|
||||
*/
|
||||
async accept(promptText) {
|
||||
assert(!this._handled, 'Cannot accept dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleDialog', {
|
||||
dialogId: this._dialogId,
|
||||
accept: true,
|
||||
promptText: promptText
|
||||
}).catch(debugError);
|
||||
}
|
||||
|
||||
async dismiss() {
|
||||
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleDialog', {
|
||||
dialogId: this._dialogId,
|
||||
accept: false
|
||||
}).catch(debugError);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Dialog};
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
class CustomError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutError extends CustomError {}
|
||||
|
||||
module.exports = {
|
||||
TimeoutError,
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
const Events = {
|
||||
Page: {
|
||||
Close: 'close',
|
||||
Console: 'console',
|
||||
Dialog: 'dialog',
|
||||
DOMContentLoaded: 'domcontentloaded',
|
||||
FrameAttached: 'frameattached',
|
||||
FrameDetached: 'framedetached',
|
||||
FrameNavigated: 'framenavigated',
|
||||
Load: 'load',
|
||||
PageError: 'pageerror',
|
||||
Popup: 'popup',
|
||||
Request: 'request',
|
||||
Response: 'response',
|
||||
RequestFinished: 'requestfinished',
|
||||
RequestFailed: 'requestfailed',
|
||||
},
|
||||
Browser: {
|
||||
Disconnected: 'disconnected',
|
||||
TargetCreated: 'targetcreated',
|
||||
TargetChanged: 'targetchanged',
|
||||
TargetDestroyed: 'targetdestroyed',
|
||||
},
|
||||
BrowserContext: {
|
||||
TargetCreated: 'targetcreated',
|
||||
TargetChanged: 'targetchanged',
|
||||
TargetDestroyed: 'targetdestroyed',
|
||||
},
|
||||
|
||||
Connection: {
|
||||
Disconnected: Symbol('Events.Connection.Disconnected'),
|
||||
},
|
||||
|
||||
JugglerSession: {
|
||||
Disconnected: Symbol('Events.JugglerSession.Disconnected'),
|
||||
},
|
||||
|
||||
FrameManager: {
|
||||
Load: Symbol('Events.FrameManager.Load'),
|
||||
DOMContentLoaded: Symbol('Events.FrameManager.DOMContentLoaded'),
|
||||
FrameAttached: Symbol('Events.FrameManager.FrameAttached'),
|
||||
FrameNavigated: Symbol('Events.FrameManager.FrameNavigated'),
|
||||
FrameDetached: Symbol('Events.FrameManager.FrameDetached'),
|
||||
},
|
||||
|
||||
NetworkManager: {
|
||||
Request: Symbol('Events.NetworkManager.Request'),
|
||||
Response: Symbol('Events.NetworkManager.Response'),
|
||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {Events};
|
|
@ -0,0 +1,103 @@
|
|||
const {helper, assert, debugError} = require('./helper');
|
||||
const {JSHandle, createHandle} = require('./JSHandle');
|
||||
|
||||
class ExecutionContext {
|
||||
/**
|
||||
* @param {!PageSession} session
|
||||
* @param {?Frame} frame
|
||||
* @param {string} executionContextId
|
||||
*/
|
||||
constructor(session, frame, executionContextId) {
|
||||
this._session = session;
|
||||
this._frame = frame;
|
||||
this._executionContextId = executionContextId;
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
if (helper.isString(pageFunction)) {
|
||||
const payload = await this._session.send('Runtime.evaluate', {
|
||||
expression: pageFunction,
|
||||
executionContextId: this._executionContextId,
|
||||
}).catch(rewriteError);
|
||||
return createHandle(this, payload.result, payload.exceptionDetails);
|
||||
}
|
||||
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 (e1) {
|
||||
// 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 (e2) {
|
||||
// We tried hard to serialize, but there's a weird beast here.
|
||||
throw new Error('Passed function is not well-serializable!');
|
||||
}
|
||||
}
|
||||
args = args.map(arg => {
|
||||
if (arg instanceof JSHandle) {
|
||||
if (arg._context !== this)
|
||||
throw new Error('JSHandles can be evaluated only in the context they were created!');
|
||||
if (arg._disposed)
|
||||
throw new Error('JSHandle is disposed!');
|
||||
return arg._protocolValue;
|
||||
}
|
||||
if (Object.is(arg, Infinity))
|
||||
return {unserializableValue: 'Infinity'};
|
||||
if (Object.is(arg, -Infinity))
|
||||
return {unserializableValue: '-Infinity'};
|
||||
if (Object.is(arg, -0))
|
||||
return {unserializableValue: '-0'};
|
||||
if (Object.is(arg, NaN))
|
||||
return {unserializableValue: 'NaN'};
|
||||
return {value: arg};
|
||||
});
|
||||
let callFunctionPromise;
|
||||
try {
|
||||
callFunctionPromise = this._session.send('Runtime.callFunction', {
|
||||
functionDeclaration: functionText,
|
||||
args,
|
||||
executionContextId: this._executionContextId
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError && err.message === 'Converting circular structure to JSON')
|
||||
err.message += ' Are you passing a nested JSHandle?';
|
||||
throw err;
|
||||
}
|
||||
const payload = await callFunctionPromise.catch(rewriteError);
|
||||
return createHandle(this, payload.result, payload.exceptionDetails);
|
||||
|
||||
function rewriteError(error) {
|
||||
if (error.message.includes('Failed to find execution context with id'))
|
||||
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
try {
|
||||
const handle = await this.evaluateHandle(pageFunction, ...args);
|
||||
const result = await handle.jsonValue();
|
||||
await handle.dispose();
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message.includes('cyclic object value') || e.message.includes('Object is not serializable'))
|
||||
return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {ExecutionContext};
|
|
@ -0,0 +1,478 @@
|
|||
const {helper, assert} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const {Events} = require('./Events');
|
||||
const {ExecutionContext} = require('./ExecutionContext');
|
||||
const {NavigationWatchdog, NextNavigationWatchdog} = require('./NavigationWatchdog');
|
||||
const {DOMWorld} = require('./DOMWorld');
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
class FrameManager extends EventEmitter {
|
||||
/**
|
||||
* @param {PageSession} session
|
||||
* @param {Page} page
|
||||
*/
|
||||
constructor(session, page, networkManager, timeoutSettings) {
|
||||
super();
|
||||
this._session = session;
|
||||
this._page = page;
|
||||
this._networkManager = networkManager;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._mainFrame = null;
|
||||
this._frames = new Map();
|
||||
/** @type {!Map<string, !ExecutionContext>} */
|
||||
this._contextIdToContext = new Map();
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
executionContextById(executionContextId) {
|
||||
return this._contextIdToContext.get(executionContextId) || null;
|
||||
}
|
||||
|
||||
_onExecutionContextCreated({executionContextId, auxData}) {
|
||||
const frameId = auxData ? auxData.frameId : null;
|
||||
const frame = this._frames.get(frameId) || null;
|
||||
const context = new ExecutionContext(this._session, frame, executionContextId);
|
||||
if (frame)
|
||||
frame._mainWorld._setContext(context);
|
||||
this._contextIdToContext.set(executionContextId, context);
|
||||
}
|
||||
|
||||
_onExecutionContextDestroyed({executionContextId}) {
|
||||
const context = this._contextIdToContext.get(executionContextId);
|
||||
if (!context)
|
||||
return;
|
||||
this._contextIdToContext.delete(executionContextId);
|
||||
if (context._frame)
|
||||
context._frame._mainWorld._setContext(null);
|
||||
}
|
||||
|
||||
frame(frameId) {
|
||||
return this._frames.get(frameId);
|
||||
}
|
||||
|
||||
mainFrame() {
|
||||
return this._mainFrame;
|
||||
}
|
||||
|
||||
frames() {
|
||||
/** @type {!Array<!Frame>} */
|
||||
let frames = [];
|
||||
collect(this._mainFrame);
|
||||
return frames;
|
||||
|
||||
function collect(frame) {
|
||||
frames.push(frame);
|
||||
for (const subframe of frame._children)
|
||||
collect(subframe);
|
||||
}
|
||||
}
|
||||
|
||||
_onNavigationCommitted(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._navigated(params.url, params.name, params.navigationId);
|
||||
frame._DOMContentLoadedFired = false;
|
||||
frame._loadFired = false;
|
||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
frame._url = params.url;
|
||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onFrameAttached(params) {
|
||||
const frame = new Frame(this._session, this, this._networkManager, this._page, params.frameId, this._timeoutSettings);
|
||||
const parentFrame = this._frames.get(params.parentFrameId) || null;
|
||||
if (parentFrame) {
|
||||
frame._parentFrame = parentFrame;
|
||||
parentFrame._children.add(frame);
|
||||
} else {
|
||||
assert(!this._mainFrame, 'INTERNAL ERROR: re-attaching main frame!');
|
||||
this._mainFrame = frame;
|
||||
}
|
||||
this._frames.set(params.frameId, frame);
|
||||
this.emit(Events.FrameManager.FrameAttached, frame);
|
||||
}
|
||||
|
||||
_onFrameDetached(params) {
|
||||
const frame = this._frames.get(params.frameId);
|
||||
this._frames.delete(params.frameId);
|
||||
frame._detach();
|
||||
this.emit(Events.FrameManager.FrameDetached, frame);
|
||||
}
|
||||
|
||||
_onEventFired({frameId, name}) {
|
||||
const frame = this._frames.get(frameId);
|
||||
frame._firedEvents.add(name.toLowerCase());
|
||||
if (frame === this._mainFrame) {
|
||||
if (name === 'load')
|
||||
this.emit(Events.FrameManager.Load);
|
||||
else if (name === 'DOMContentLoaded')
|
||||
this.emit(Events.FrameManager.DOMContentLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
class Frame {
|
||||
/**
|
||||
* @param {*} session
|
||||
* @param {!Page} page
|
||||
* @param {string} frameId
|
||||
*/
|
||||
constructor(session, frameManager, networkManager, page, frameId, timeoutSettings) {
|
||||
this._session = session;
|
||||
this._page = page;
|
||||
this._frameManager = frameManager;
|
||||
this._networkManager = networkManager;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._frameId = frameId;
|
||||
/** @type {?Frame} */
|
||||
this._parentFrame = null;
|
||||
this._url = '';
|
||||
this._name = '';
|
||||
/** @type {!Set<!Frame>} */
|
||||
this._children = new Set();
|
||||
this._detached = false;
|
||||
|
||||
|
||||
this._firedEvents = new Set();
|
||||
|
||||
this._mainWorld = new DOMWorld(this, timeoutSettings);
|
||||
}
|
||||
|
||||
async executionContext() {
|
||||
return this._mainWorld.executionContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async waitForNavigation(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const nextNavigationDog = new NextNavigationWatchdog(this._session, this);
|
||||
const error1 = await Promise.race([
|
||||
nextNavigationDog.promise(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
nextNavigationDog.dispose();
|
||||
|
||||
// If timeout happened first - throw.
|
||||
if (error1) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error1;
|
||||
}
|
||||
|
||||
const {navigationId, url} = nextNavigationDog.navigation();
|
||||
|
||||
if (!navigationId) {
|
||||
// Same document navigation happened.
|
||||
clearTimeout(timeoutId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goto(url, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
referer,
|
||||
} = options;
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId} = await this._session.send('Page.navigate', {
|
||||
frameId: this._frameId,
|
||||
referer,
|
||||
url,
|
||||
});
|
||||
if (!navigationId)
|
||||
return;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, this, this._networkManager, navigationId, url, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
return this._mainWorld.click(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
return this._mainWorld.tap(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
return this._mainWorld.type(selector, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
return this._mainWorld.focus(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
return this._mainWorld.hover(selector);
|
||||
}
|
||||
|
||||
_detach() {
|
||||
this._parentFrame._children.delete(this);
|
||||
this._parentFrame = null;
|
||||
this._detached = true;
|
||||
this._mainWorld._detach();
|
||||
}
|
||||
|
||||
_navigated(url, name, navigationId) {
|
||||
this._url = url;
|
||||
this._name = name;
|
||||
this._lastCommittedNavigationId = navigationId;
|
||||
this._firedEvents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values) {
|
||||
return this._mainWorld.select(selector, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
||||
* @param {!{polling?: string|number, timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitFor(selectorOrFunctionOrTimeout, options, ...args) {
|
||||
const xPathPattern = '//';
|
||||
|
||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
||||
if (string.startsWith(xPathPattern))
|
||||
return this.waitForXPath(string, options);
|
||||
return this.waitForSelector(string, options);
|
||||
}
|
||||
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
||||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
||||
if (typeof selectorOrFunctionOrTimeout === 'function')
|
||||
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
||||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForSelector(selector, options) {
|
||||
return this._mainWorld.waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
waitForXPath(xpath, options) {
|
||||
return this._mainWorld.waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
async content() {
|
||||
return this._mainWorld.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
return this._mainWorld.setContent(html);
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
return this._mainWorld.$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
return this._mainWorld.$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
return this._mainWorld.$x(expression);
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
return this._mainWorld.addScriptTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
return this._mainWorld.addStyleTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this._mainWorld.title();
|
||||
}
|
||||
|
||||
name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
isDetached() {
|
||||
return this._detached;
|
||||
}
|
||||
|
||||
childFrames() {
|
||||
return Array.from(this._children);
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
parentFrame() {
|
||||
return this._parentFrame;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWaitUntil(waitUntil) {
|
||||
if (!Array.isArray(waitUntil))
|
||||
waitUntil = [waitUntil];
|
||||
for (const condition of waitUntil) {
|
||||
if (condition !== 'load' && condition !== 'domcontentloaded')
|
||||
throw new Error('Unknown waitUntil condition: ' + condition);
|
||||
}
|
||||
return waitUntil;
|
||||
}
|
||||
|
||||
module.exports = {FrameManager, Frame, normalizeWaitUntil};
|
|
@ -0,0 +1,331 @@
|
|||
/**
|
||||
* 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 keyDefinitions = require('./USKeyboardLayout');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDescription
|
||||
* @property {number} keyCode
|
||||
* @property {string} key
|
||||
* @property {string} text
|
||||
* @property {string} code
|
||||
* @property {number} location
|
||||
*/
|
||||
|
||||
class Keyboard {
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._modifiers = 0;
|
||||
this._pressedKeys = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
async down(key) {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
|
||||
const repeat = this._pressedKeys.has(description.code);
|
||||
this._pressedKeys.add(description.code);
|
||||
this._modifiers |= this._modifierBit(description.key);
|
||||
|
||||
await this._client.send('Page.dispatchKeyEvent', {
|
||||
type: 'keydown',
|
||||
keyCode: description.keyCode,
|
||||
code: description.code,
|
||||
key: description.key,
|
||||
repeat,
|
||||
location: description.location
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {number}
|
||||
*/
|
||||
_modifierBit(key) {
|
||||
if (key === 'Alt')
|
||||
return 1;
|
||||
if (key === 'Control')
|
||||
return 2;
|
||||
if (key === 'Shift')
|
||||
return 4;
|
||||
if (key === 'Meta')
|
||||
return 8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} keyString
|
||||
* @return {KeyDescription}
|
||||
*/
|
||||
_keyDescriptionForString(keyString) {
|
||||
const shift = this._modifiers & 8;
|
||||
const description = {
|
||||
key: '',
|
||||
keyCode: 0,
|
||||
code: '',
|
||||
text: '',
|
||||
location: 0
|
||||
};
|
||||
const definition = keyDefinitions[keyString];
|
||||
if (!definition)
|
||||
throw new Error(`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 = '';
|
||||
|
||||
if (description.code === 'MetaLeft')
|
||||
description.code = 'OSLeft';
|
||||
if (description.code === 'MetaRight')
|
||||
description.code = 'OSRight';
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
async up(key) {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
|
||||
this._modifiers &= ~this._modifierBit(description.key);
|
||||
this._pressedKeys.delete(description.code);
|
||||
await this._client.send('Page.dispatchKeyEvent', {
|
||||
type: 'keyup',
|
||||
key: description.key,
|
||||
keyCode: description.keyCode,
|
||||
code: description.code,
|
||||
location: description.location,
|
||||
repeat: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} char
|
||||
*/
|
||||
async sendCharacter(char) {
|
||||
await this._client.send('Page.insertText', {
|
||||
text: char
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async type(text, options = {}) {
|
||||
const {delay = null} = options;
|
||||
for (const char of text) {
|
||||
if (keyDefinitions[char])
|
||||
await this.press(char, {delay});
|
||||
else
|
||||
await this.sendCharacter(char);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async press(key, options = {}) {
|
||||
const {delay = null} = options;
|
||||
await this.down(key);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, options.delay));
|
||||
await this.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
class Mouse {
|
||||
/**
|
||||
* @param {!Keyboard} keyboard
|
||||
*/
|
||||
constructor(client, keyboard) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this._x = 0;
|
||||
this._y = 0;
|
||||
this._buttons = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {{steps?: number}=} options
|
||||
*/
|
||||
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('Page.dispatchMouseEvent', {
|
||||
type: 'mousemove',
|
||||
button: 0,
|
||||
x: fromX + (this._x - fromX) * (i / steps),
|
||||
y: fromY + (this._y - fromY) * (i / steps),
|
||||
modifiers: this._keyboard._modifiers,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(x, y, options = {}) {
|
||||
const {delay = null} = options;
|
||||
this.move(x, y);
|
||||
this.down(options);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
await this.up(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async down(options = {}) {
|
||||
const {
|
||||
button = "left",
|
||||
clickCount = 1
|
||||
} = options;
|
||||
if (button === 'left')
|
||||
this._buttons |= 1;
|
||||
if (button === 'right')
|
||||
this._buttons |= 2;
|
||||
if (button === 'middle')
|
||||
this._buttons |= 4;
|
||||
await this._client.send('Page.dispatchMouseEvent', {
|
||||
type: 'mousedown',
|
||||
button: this._buttonNameToButton(button),
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
clickCount,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} buttonName
|
||||
* @return {number}
|
||||
*/
|
||||
_buttonNameToButton(buttonName) {
|
||||
if (buttonName === 'left')
|
||||
return 0;
|
||||
if (buttonName === 'middle')
|
||||
return 1;
|
||||
if (buttonName === 'right')
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async up(options = {}) {
|
||||
const {
|
||||
button = "left",
|
||||
clickCount = 1
|
||||
} = options;
|
||||
if (button === 'left')
|
||||
this._buttons &= ~1;
|
||||
if (button === 'right')
|
||||
this._buttons &= ~2;
|
||||
if (button === 'middle')
|
||||
this._buttons &= ~4;
|
||||
await this._client.send('Page.dispatchMouseEvent', {
|
||||
type: 'mouseup',
|
||||
button: this._buttonNameToButton(button),
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
clickCount: clickCount,
|
||||
buttons: this._buttons,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Touchscreen {
|
||||
/**
|
||||
* @param {Puppeteer.JugglerSession} client
|
||||
* @param {Keyboard} keyboard
|
||||
* @param {Mouse} mouse
|
||||
*/
|
||||
constructor(client, keyboard, mouse) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this._mouse = mouse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
async tap(x, y) {
|
||||
const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
|
||||
let {defaultPrevented} = (await this._client.send('Page.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints,
|
||||
modifiers: this._keyboard._modifiers
|
||||
}));
|
||||
defaultPrevented = (await this._client.send('Page.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints,
|
||||
modifiers: this._keyboard._modifiers
|
||||
})).defaultPrevented || defaultPrevented;
|
||||
// Do not dispatch related mouse events if either of touch events
|
||||
// were prevented.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent#Event_order
|
||||
if (defaultPrevented)
|
||||
return;
|
||||
await this._mouse.move(x, y);
|
||||
await this._mouse.down();
|
||||
await this._mouse.up();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Keyboard, Mouse, Touchscreen };
|
|
@ -0,0 +1,435 @@
|
|||
const {assert, debugError} = require('./helper');
|
||||
const path = require('path');
|
||||
|
||||
class JSHandle {
|
||||
|
||||
/**
|
||||
* @param {!ExecutionContext} context
|
||||
* @param {*} payload
|
||||
*/
|
||||
constructor(context, payload) {
|
||||
this._context = context;
|
||||
this._session = this._context._session;
|
||||
this._executionContextId = this._context._executionContextId;
|
||||
this._objectId = payload.objectId;
|
||||
this._type = payload.type;
|
||||
this._subtype = payload.subtype;
|
||||
this._disposed = false;
|
||||
this._protocolValue = {
|
||||
unserializableValue: payload.unserializableValue,
|
||||
value: payload.value,
|
||||
objectId: payload.objectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ExecutionContext}
|
||||
*/
|
||||
executionContext() {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {string}
|
||||
*/
|
||||
toString() {
|
||||
if (this._objectId)
|
||||
return 'JSHandle@' + (this._subtype || this._type);
|
||||
return 'JSHandle:' + this._deserializeValue(this._protocolValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} propertyName
|
||||
* @return {!Promise<?JSHandle>}
|
||||
*/
|
||||
async getProperty(propertyName) {
|
||||
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
|
||||
const result = {__proto__: null};
|
||||
result[propertyName] = object[propertyName];
|
||||
return result;
|
||||
}, this, propertyName);
|
||||
const properties = await objectHandle.getProperties();
|
||||
const result = properties.get(propertyName) || null;
|
||||
await objectHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<Map<string, !JSHandle>>}
|
||||
*/
|
||||
async getProperties() {
|
||||
const response = await this._session.send('Runtime.getObjectProperties', {
|
||||
executionContextId: this._executionContextId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
const result = new Map();
|
||||
for (const property of response.properties) {
|
||||
result.set(property.name, createHandle(this._context, property.value, null));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_deserializeValue({unserializableValue, value}) {
|
||||
if (unserializableValue === 'Infinity')
|
||||
return Infinity;
|
||||
if (unserializableValue === '-Infinity')
|
||||
return -Infinity;
|
||||
if (unserializableValue === '-0')
|
||||
return -0;
|
||||
if (unserializableValue === 'NaN')
|
||||
return NaN;
|
||||
return value;
|
||||
}
|
||||
|
||||
async jsonValue() {
|
||||
if (!this._objectId)
|
||||
return this._deserializeValue(this._protocolValue);
|
||||
const simpleValue = await this._session.send('Runtime.callFunction', {
|
||||
executionContextId: this._executionContextId,
|
||||
returnByValue: true,
|
||||
functionDeclaration: (e => e).toString(),
|
||||
args: [this._protocolValue],
|
||||
});
|
||||
return this._deserializeValue(simpleValue.result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (!this._objectId)
|
||||
return;
|
||||
this._disposed = true;
|
||||
await this._session.send('Runtime.disposeObject', {
|
||||
executionContextId: this._executionContextId,
|
||||
objectId: this._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.
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ElementHandle extends JSHandle {
|
||||
/**
|
||||
* @param {Frame} frame
|
||||
* @param {ExecutionContext} context
|
||||
* @param {*} payload
|
||||
*/
|
||||
constructor(frame, context, payload) {
|
||||
super(context, payload);
|
||||
this._frame = frame;
|
||||
this._frameId = frame._frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Frame}
|
||||
*/
|
||||
async contentFrame() {
|
||||
const {frameId} = await this._session.send('Page.contentFrame', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
if (!frameId)
|
||||
return null;
|
||||
const frame = this._frame._frameManager.frame(frameId);
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {!ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<{width: number, height: number, x: number, y: number}>}
|
||||
*/
|
||||
async boundingBox() {
|
||||
return await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{encoding?: string, path?: string}} options
|
||||
*/
|
||||
async screenshot(options = {}) {
|
||||
const clip = await this._session.send('Page.getBoundingBox', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
});
|
||||
if (!clip)
|
||||
throw new Error('Node is either not visible or not an HTMLElement');
|
||||
assert(clip.width, 'Node has 0 width.');
|
||||
assert(clip.height, 'Node has 0 height.');
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
|
||||
return await this._frame._page.screenshot(Object.assign({}, options, {
|
||||
clip: {
|
||||
x: clip.x,
|
||||
y: clip.y,
|
||||
width: clip.width,
|
||||
height: clip.height,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {!Promise<boolean>}
|
||||
*/
|
||||
isIntersectingViewport() {
|
||||
return this._frame.evaluate(async element => {
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
return visibleRatio > 0;
|
||||
}, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
const handle = await this._frame.evaluateHandle(
|
||||
(element, selector) => element.querySelector(selector),
|
||||
this, selector
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(element, selector) => element.querySelectorAll(selector),
|
||||
this, selector
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
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 this._frame.evaluate(pageFunction, elementHandle, ...args);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const arrayHandle = await this._frame.evaluateHandle(
|
||||
(element, selector) => Array.from(element.querySelectorAll(selector)),
|
||||
this, selector
|
||||
);
|
||||
|
||||
const result = await this._frame.evaluate(pageFunction, arrayHandle, ...args);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const arrayHandle = await this._frame.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;
|
||||
},
|
||||
this, 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 _scrollIntoViewIfNeeded() {
|
||||
const error = await this._frame.evaluate(async(element) => {
|
||||
if (!element.isConnected)
|
||||
return 'Node is detached from document';
|
||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
const visibleRatio = await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
resolve(entries[0].intersectionRatio);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element);
|
||||
// Firefox doesn't call IntersectionObserver callback unless
|
||||
// there are rafs.
|
||||
requestAnimationFrame(() => {});
|
||||
});
|
||||
if (visibleRatio !== 1.0)
|
||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
||||
return false;
|
||||
}, this);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(options) {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
async tap() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.touchscreen.tap(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} filePaths
|
||||
*/
|
||||
async uploadFile(...filePaths) {
|
||||
const files = filePaths.map(filePath => path.resolve(filePath));
|
||||
await this._session.send('Page.setFileInputFiles', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
async hover() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._frame._page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this._frame.evaluate(element => element.focus(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(text, options) {
|
||||
await this.focus();
|
||||
await this._frame._page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number}=} options
|
||||
*/
|
||||
async press(key, options) {
|
||||
await this.focus();
|
||||
await this._frame._page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Promise<!{x: number, y: number}>}
|
||||
*/
|
||||
async _clickablePoint() {
|
||||
const result = await this._session.send('Page.getContentQuads', {
|
||||
frameId: this._frameId,
|
||||
objectId: this._objectId,
|
||||
}).catch(debugError);
|
||||
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 quads = result.quads.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.
|
||||
return computeQuadCenter(quads[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function createHandle(context, result, exceptionDetails) {
|
||||
const frame = context.frame();
|
||||
if (exceptionDetails) {
|
||||
if (exceptionDetails.value)
|
||||
throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value));
|
||||
else
|
||||
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
|
||||
}
|
||||
return result.subtype === 'node' ? new ElementHandle(frame, context, result) : new JSHandle(context, result);
|
||||
}
|
||||
|
||||
function computeQuadArea(quad) {
|
||||
// Compute sum of all directed areas of adjacent triangles
|
||||
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
|
||||
let area = 0;
|
||||
const points = [quad.p1, quad.p2, quad.p3, quad.p4];
|
||||
for (let i = 0; i < points.length; ++i) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
||||
}
|
||||
return Math.abs(area);
|
||||
}
|
||||
|
||||
function computeQuadCenter(quad) {
|
||||
let x = 0, y = 0;
|
||||
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
|
||||
x += point.x;
|
||||
y += point.y;
|
||||
}
|
||||
return {x: x / 4, y: y / 4};
|
||||
}
|
||||
|
||||
|
||||
module.exports = {JSHandle, ElementHandle, createHandle};
|
|
@ -0,0 +1,295 @@
|
|||
/**
|
||||
* 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 removeFolder = require('rimraf');
|
||||
const childProcess = require('child_process');
|
||||
const {Connection} = require('./Connection');
|
||||
const {Browser} = require('./Browser');
|
||||
const {BrowserFetcher} = require('./BrowserFetcher');
|
||||
const readline = require('readline');
|
||||
const fs = require('fs');
|
||||
const util = require('util');
|
||||
const {helper, debugError} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors')
|
||||
const WebSocketTransport = require('./WebSocketTransport');
|
||||
|
||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||
const removeFolderAsync = util.promisify(removeFolder);
|
||||
|
||||
const FIREFOX_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_firefox_profile-');
|
||||
|
||||
const DEFAULT_ARGS = [
|
||||
'-no-remote',
|
||||
'-foreground',
|
||||
];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Launcher {
|
||||
constructor(projectRoot, preferredRevision) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._preferredRevision = preferredRevision;
|
||||
}
|
||||
|
||||
defaultArgs(options = {}) {
|
||||
const {
|
||||
headless = true,
|
||||
args = [],
|
||||
userDataDir = null,
|
||||
} = options;
|
||||
const firefoxArguments = [...DEFAULT_ARGS];
|
||||
if (userDataDir)
|
||||
firefoxArguments.push('-profile', userDataDir);
|
||||
if (headless)
|
||||
firefoxArguments.push('-headless');
|
||||
firefoxArguments.push(...args);
|
||||
if (args.every(arg => arg.startsWith('-')))
|
||||
firefoxArguments.push('about:blank');
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
async launch(options = {}) {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
dumpio = false,
|
||||
executablePath = null,
|
||||
env = process.env,
|
||||
handleSIGHUP = true,
|
||||
handleSIGINT = true,
|
||||
handleSIGTERM = true,
|
||||
ignoreHTTPSErrors = false,
|
||||
headless = true,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
slowMo = 0,
|
||||
timeout = 30000,
|
||||
} = 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.includes('-juggler'))
|
||||
firefoxArguments.push('-juggler', '0');
|
||||
|
||||
let temporaryProfileDir = null;
|
||||
if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
|
||||
temporaryProfileDir = await mkdtempAsync(FIREFOX_PROFILE_PATH);
|
||||
firefoxArguments.push(`-profile`, temporaryProfileDir);
|
||||
}
|
||||
|
||||
let firefoxExecutable = executablePath;
|
||||
if (!firefoxExecutable) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
firefoxExecutable = executablePath;
|
||||
}
|
||||
const stdio = ['pipe', 'pipe', 'pipe'];
|
||||
const firefoxProcess = childProcess.spawn(
|
||||
firefoxExecutable,
|
||||
firefoxArguments,
|
||||
{
|
||||
// On non-windows platforms, `detached: false` 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',
|
||||
stdio,
|
||||
// On linux Juggler ships the libstdc++ it was linked against.
|
||||
env: os.platform() === 'linux' ? {
|
||||
...env,
|
||||
LD_LIBRARY_PATH: `${path.dirname(firefoxExecutable)}:${process.env.LD_LIBRARY_PATH}`,
|
||||
} : env,
|
||||
}
|
||||
);
|
||||
|
||||
if (dumpio) {
|
||||
firefoxProcess.stderr.pipe(process.stderr);
|
||||
firefoxProcess.stdout.pipe(process.stdout);
|
||||
}
|
||||
|
||||
let firefoxClosed = false;
|
||||
const waitForFirefoxToClose = new Promise((fulfill, reject) => {
|
||||
firefoxProcess.once('exit', () => {
|
||||
firefoxClosed = true;
|
||||
// Cleanup as processes exit.
|
||||
if (temporaryProfileDir) {
|
||||
removeFolderAsync(temporaryProfileDir)
|
||||
.then(() => fulfill())
|
||||
.catch(err => console.error(err));
|
||||
} else {
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const listeners = [ helper.addEventListener(process, 'exit', killFirefox) ];
|
||||
if (handleSIGINT)
|
||||
listeners.push(helper.addEventListener(process, 'SIGINT', () => { killFirefox(); process.exit(130); }));
|
||||
if (handleSIGTERM)
|
||||
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseFirefox));
|
||||
if (handleSIGHUP)
|
||||
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseFirefox));
|
||||
/** @type {?Connection} */
|
||||
let connection = null;
|
||||
try {
|
||||
const url = await waitForWSEndpoint(firefoxProcess, timeout);
|
||||
const transport = await WebSocketTransport.create(url);
|
||||
connection = new Connection(url, transport, slowMo);
|
||||
const browser = await Browser.create(connection, defaultViewport, firefoxProcess, gracefullyCloseFirefox);
|
||||
if (ignoreHTTPSErrors)
|
||||
await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true});
|
||||
await browser.waitForTarget(t => t.type() === 'page');
|
||||
return browser;
|
||||
} catch (e) {
|
||||
killFirefox();
|
||||
throw e;
|
||||
}
|
||||
|
||||
function gracefullyCloseFirefox() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (temporaryProfileDir) {
|
||||
killFirefox();
|
||||
} else if (connection) {
|
||||
connection.send('Browser.close').catch(error => {
|
||||
debugError(error);
|
||||
killFirefox();
|
||||
});
|
||||
}
|
||||
return waitForFirefoxToClose;
|
||||
}
|
||||
|
||||
// This method has to be sync to be used as 'exit' event handler.
|
||||
function killFirefox() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (firefoxProcess.pid && !firefoxProcess.killed && !firefoxClosed) {
|
||||
// Force kill chrome.
|
||||
try {
|
||||
if (process.platform === 'win32')
|
||||
childProcess.execSync(`taskkill /pid ${firefoxProcess.pid} /T /F`);
|
||||
else
|
||||
process.kill(-firefoxProcess.pid, 'SIGKILL');
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
}
|
||||
// Attempt to remove temporary profile directory to avoid littering.
|
||||
try {
|
||||
removeFolder.sync(temporaryProfileDir);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
async connect(options = {}) {
|
||||
const {
|
||||
browserWSEndpoint,
|
||||
slowMo = 0,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
ignoreHTTPSErrors = false,
|
||||
} = options;
|
||||
let connection = null;
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
connection = new Connection(browserWSEndpoint, transport, slowMo);
|
||||
const browser = await Browser.create(connection, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
|
||||
if (ignoreHTTPSErrors)
|
||||
await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true});
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
executablePath() {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
_resolveExecutablePath() {
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot, { product: 'firefox' });
|
||||
const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.ChildProcess} firefoxProcess
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
function waitForWSEndpoint(firefoxProcess, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rl = readline.createInterface({ input: firefoxProcess.stdout });
|
||||
let stderr = '';
|
||||
const listeners = [
|
||||
helper.addEventListener(rl, 'line', onLine),
|
||||
helper.addEventListener(rl, 'close', () => onClose()),
|
||||
helper.addEventListener(firefoxProcess, 'exit', () => onClose()),
|
||||
helper.addEventListener(firefoxProcess, 'error', error => onClose(error))
|
||||
];
|
||||
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
|
||||
|
||||
/**
|
||||
* @param {!Error=} error
|
||||
*/
|
||||
function onClose(error) {
|
||||
cleanup();
|
||||
reject(new Error([
|
||||
'Failed to launch Firefox!' + (error ? ' ' + error.message : ''),
|
||||
stderr,
|
||||
'',
|
||||
].join('\n')));
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
cleanup();
|
||||
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
function onLine(line) {
|
||||
stderr += line + '\n';
|
||||
const match = line.match(/^Juggler listening on (ws:\/\/.*)$/);
|
||||
if (!match)
|
||||
return;
|
||||
cleanup();
|
||||
resolve(match[1]);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (timeoutId)
|
||||
clearTimeout(timeoutId);
|
||||
helper.removeEventListeners(listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {Launcher};
|
|
@ -0,0 +1,119 @@
|
|||
const {helper} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NextNavigationWatchdog {
|
||||
constructor(session, navigatedFrame) {
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._navigation = null;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||
helper.addEventListener(session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
navigation() {
|
||||
return this._navigation;
|
||||
}
|
||||
|
||||
_onNavigationStarted(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId) {
|
||||
this._navigation = {
|
||||
navigationId: params.navigationId,
|
||||
url: params.url,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
_onSameDocumentNavigation(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId) {
|
||||
this._navigation = {
|
||||
navigationId: null,
|
||||
};
|
||||
this._resolveCallback();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NavigationWatchdog {
|
||||
constructor(session, navigatedFrame, networkManager, targetNavigationId, targetURL, firedEvents) {
|
||||
this._navigatedFrame = navigatedFrame;
|
||||
this._targetNavigationId = targetNavigationId;
|
||||
this._firedEvents = firedEvents;
|
||||
this._targetURL = targetURL;
|
||||
|
||||
this._promise = new Promise(x => this._resolveCallback = x);
|
||||
this._navigationRequest = null;
|
||||
|
||||
const check = this._checkNavigationComplete.bind(this);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, Events.JugglerSession.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))),
|
||||
helper.addEventListener(session, 'Page.eventFired', check),
|
||||
helper.addEventListener(session, 'Page.frameAttached', check),
|
||||
helper.addEventListener(session, 'Page.frameDetached', check),
|
||||
helper.addEventListener(session, 'Page.navigationStarted', check),
|
||||
helper.addEventListener(session, 'Page.navigationCommitted', check),
|
||||
helper.addEventListener(session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||
helper.addEventListener(networkManager, Events.NetworkManager.Request, this._onRequest.bind(this)),
|
||||
helper.addEventListener(navigatedFrame._frameManager, Events.FrameManager.FrameDetached, check),
|
||||
];
|
||||
check();
|
||||
}
|
||||
|
||||
_onRequest(request) {
|
||||
if (request.frame() !== this._navigatedFrame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
navigationResponse() {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
_checkNavigationComplete() {
|
||||
if (this._navigatedFrame.isDetached()) {
|
||||
this._resolveCallback(new Error('Navigating frame was detached'));
|
||||
} else if (this._navigatedFrame._lastCommittedNavigationId === this._targetNavigationId
|
||||
&& checkFiredEvents(this._navigatedFrame, this._firedEvents)) {
|
||||
this._resolveCallback(null);
|
||||
}
|
||||
|
||||
function checkFiredEvents(frame, firedEvents) {
|
||||
for (const subframe of frame._children) {
|
||||
if (!checkFiredEvents(subframe, firedEvents))
|
||||
return false;
|
||||
}
|
||||
return firedEvents.every(event => frame._firedEvents.has(event));
|
||||
}
|
||||
}
|
||||
|
||||
_onNavigationAborted(params) {
|
||||
if (params.frameId === this._navigatedFrame._frameId && params.navigationId === this._targetNavigationId)
|
||||
this._resolveCallback(new Error('Navigation to ' + this._targetURL + ' failed: ' + params.errorText));
|
||||
}
|
||||
|
||||
promise() {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {NavigationWatchdog, NextNavigationWatchdog};
|
|
@ -0,0 +1,356 @@
|
|||
const {helper, assert, debugError} = require('./helper');
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
class NetworkManager extends EventEmitter {
|
||||
constructor(session) {
|
||||
super();
|
||||
this._session = session;
|
||||
|
||||
this._requests = new Map();
|
||||
this._frameManager = null;
|
||||
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)),
|
||||
helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
|
||||
helper.addEventListener(session, 'Network.requestFinished', this._onRequestFinished.bind(this)),
|
||||
helper.addEventListener(session, 'Network.requestFailed', this._onRequestFailed.bind(this)),
|
||||
];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
setFrameManager(frameManager) {
|
||||
this._frameManager = frameManager;
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
const array = [];
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
|
||||
array.push({name, value});
|
||||
}
|
||||
await this._session.send('Network.setExtraHTTPHeaders', {headers: array});
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled) {
|
||||
await this._session.send('Network.setRequestInterception', {enabled});
|
||||
}
|
||||
|
||||
_onRequestWillBeSent(event) {
|
||||
const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null;
|
||||
const frame = redirected ? redirected.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null);
|
||||
if (!frame)
|
||||
return;
|
||||
let redirectChain = [];
|
||||
if (redirected) {
|
||||
redirectChain = redirected._redirectChain;
|
||||
redirectChain.push(redirected);
|
||||
this._requests.delete(redirected._id);
|
||||
}
|
||||
const request = new Request(this._session, frame, redirectChain, event);
|
||||
this._requests.set(request._id, request);
|
||||
this.emit(Events.NetworkManager.Request, request);
|
||||
}
|
||||
|
||||
_onResponseReceived(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
const response = new Response(this._session, request, event);
|
||||
request._response = response;
|
||||
this.emit(Events.NetworkManager.Response, response);
|
||||
}
|
||||
|
||||
_onRequestFinished(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
// Keep redirected requests in the map for future reference in redirectChain.
|
||||
const isRedirected = request.response().status() >= 300 && request.response().status() <= 399;
|
||||
if (isRedirected) {
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
||||
} else {
|
||||
this._requests.delete(request._id);
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||
}
|
||||
this.emit(Events.NetworkManager.RequestFinished, request);
|
||||
}
|
||||
|
||||
_onRequestFailed(event) {
|
||||
const request = this._requests.get(event.requestId);
|
||||
if (!request)
|
||||
return;
|
||||
this._requests.delete(request._id);
|
||||
if (request.response())
|
||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||
request._errorText = event.errorCode;
|
||||
this.emit(Events.NetworkManager.RequestFailed, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* document, stylesheet, image, media, font, script, texttrack, xhr, fetch, eventsource, websocket, manifest, other.
|
||||
*/
|
||||
const causeToResourceType = {
|
||||
TYPE_INVALID: 'other',
|
||||
TYPE_OTHER: 'other',
|
||||
TYPE_SCRIPT: 'script',
|
||||
TYPE_IMAGE: 'image',
|
||||
TYPE_STYLESHEET: 'stylesheet',
|
||||
TYPE_OBJECT: 'other',
|
||||
TYPE_DOCUMENT: 'document',
|
||||
TYPE_SUBDOCUMENT: 'document',
|
||||
TYPE_REFRESH: 'document',
|
||||
TYPE_XBL: 'other',
|
||||
TYPE_PING: 'other',
|
||||
TYPE_XMLHTTPREQUEST: 'xhr',
|
||||
TYPE_OBJECT_SUBREQUEST: 'other',
|
||||
TYPE_DTD: 'other',
|
||||
TYPE_FONT: 'font',
|
||||
TYPE_MEDIA: 'media',
|
||||
TYPE_WEBSOCKET: 'websocket',
|
||||
TYPE_CSP_REPORT: 'other',
|
||||
TYPE_XSLT: 'other',
|
||||
TYPE_BEACON: 'other',
|
||||
TYPE_FETCH: 'fetch',
|
||||
TYPE_IMAGESET: 'images',
|
||||
TYPE_WEB_MANIFEST: 'manifest',
|
||||
};
|
||||
|
||||
class Request {
|
||||
constructor(session, frame, redirectChain, payload) {
|
||||
this._session = session;
|
||||
this._frame = frame;
|
||||
this._id = payload.requestId;
|
||||
this._redirectChain = redirectChain;
|
||||
this._url = payload.url;
|
||||
this._postData = payload.postData;
|
||||
this._suspended = payload.suspended;
|
||||
this._response = null;
|
||||
this._errorText = null;
|
||||
this._isNavigationRequest = payload.isNavigationRequest;
|
||||
this._method = payload.method;
|
||||
this._resourceType = causeToResourceType[payload.cause] || 'other';
|
||||
this._headers = {};
|
||||
this._interceptionHandled = false;
|
||||
for (const {name, value} of payload.headers)
|
||||
this._headers[name.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
failure() {
|
||||
return this._errorText ? {errorText: this._errorText} : null;
|
||||
}
|
||||
|
||||
async continue(overrides = {}) {
|
||||
assert(!overrides.url, 'Puppeteer-Firefox does not support overriding URL');
|
||||
assert(!overrides.method, 'Puppeteer-Firefox does not support overriding method');
|
||||
assert(!overrides.postData, 'Puppeteer-Firefox does not support overriding postData');
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
const {
|
||||
headers,
|
||||
} = overrides;
|
||||
await this._session.send('Network.resumeSuspendedRequest', {
|
||||
requestId: this._id,
|
||||
headers: headers ? Object.entries(headers).map(([name, value]) => ({name, value})) : undefined,
|
||||
}).catch(error => {
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
async abort() {
|
||||
assert(this._suspended, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
await this._session.send('Network.abortSuspendedRequest', {
|
||||
requestId: this._id,
|
||||
}).catch(error => {
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
postData() {
|
||||
return this._postData;
|
||||
}
|
||||
|
||||
headers() {
|
||||
return {...this._headers};
|
||||
}
|
||||
|
||||
redirectChain() {
|
||||
return this._redirectChain.slice();
|
||||
}
|
||||
|
||||
resourceType() {
|
||||
return this._resourceType;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
method() {
|
||||
return this._method;
|
||||
}
|
||||
|
||||
isNavigationRequest() {
|
||||
return this._isNavigationRequest;
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
response() {
|
||||
return this._response;
|
||||
}
|
||||
}
|
||||
|
||||
class Response {
|
||||
constructor(session, request, payload) {
|
||||
this._session = session;
|
||||
this._request = request;
|
||||
this._remoteIPAddress = payload.remoteIPAddress;
|
||||
this._remotePort = payload.remotePort;
|
||||
this._status = payload.status;
|
||||
this._statusText = payload.statusText;
|
||||
this._headers = {};
|
||||
this._securityDetails = payload.securityDetails ? new SecurityDetails(payload.securityDetails) : null;
|
||||
for (const {name, value} of payload.headers)
|
||||
this._headers[name.toLowerCase()] = value;
|
||||
this._bodyLoadedPromise = new Promise(fulfill => {
|
||||
this._bodyLoadedPromiseFulfill = fulfill;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Buffer>}
|
||||
*/
|
||||
buffer() {
|
||||
if (!this._contentPromise) {
|
||||
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
||||
if (error)
|
||||
throw error;
|
||||
const response = await this._session.send('Network.getResponseBody', {
|
||||
requestId: this._request._id
|
||||
});
|
||||
if (response.evicted)
|
||||
throw new Error(`Response body for ${this._request.method()} ${this._request.url()} was evicted!`);
|
||||
return Buffer.from(response.base64body, 'base64');
|
||||
});
|
||||
}
|
||||
return this._contentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async text() {
|
||||
const content = await this.buffer();
|
||||
return content.toString('utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Object>}
|
||||
*/
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
securityDetails() {
|
||||
return this._securityDetails;
|
||||
}
|
||||
|
||||
headers() {
|
||||
return {...this._headers};
|
||||
}
|
||||
|
||||
status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
statusText() {
|
||||
return this._statusText;
|
||||
}
|
||||
|
||||
ok() {
|
||||
return this._status >= 200 && this._status <= 299;
|
||||
}
|
||||
|
||||
remoteAddress() {
|
||||
return {
|
||||
ip: this._remoteIPAddress,
|
||||
port: this._remotePort,
|
||||
};
|
||||
}
|
||||
|
||||
frame() {
|
||||
return this._request.frame();
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._request.url();
|
||||
}
|
||||
|
||||
request() {
|
||||
return this._request;
|
||||
}
|
||||
}
|
||||
|
||||
class SecurityDetails {
|
||||
/**
|
||||
* @param {!Protocol.Network.SecurityDetails} securityPayload
|
||||
*/
|
||||
constructor(securityPayload) {
|
||||
this._subjectName = securityPayload['subjectName'];
|
||||
this._issuer = securityPayload['issuer'];
|
||||
this._validFrom = securityPayload['validFrom'];
|
||||
this._validTo = securityPayload['validTo'];
|
||||
this._protocol = securityPayload['protocol'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
subjectName() {
|
||||
return this._subjectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
issuer() {
|
||||
return this._issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validFrom() {
|
||||
return this._validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validTo() {
|
||||
return this._validTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {NetworkManager, Request, Response, SecurityDetails};
|
|
@ -0,0 +1,805 @@
|
|||
const {helper, debugError, assert} = require('./helper');
|
||||
const {Keyboard, Mouse, Touchscreen} = require('./Input');
|
||||
const {Dialog} = require('./Dialog');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const fs = require('fs');
|
||||
const mime = require('mime');
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const {createHandle} = require('./JSHandle');
|
||||
const {Events} = require('./Events');
|
||||
const {Connection} = require('./Connection');
|
||||
const {FrameManager, normalizeWaitUntil} = require('./FrameManager');
|
||||
const {NetworkManager} = require('./NetworkManager');
|
||||
const {TimeoutSettings} = require('./TimeoutSettings');
|
||||
const {NavigationWatchdog} = require('./NavigationWatchdog');
|
||||
const {Accessibility} = require('./Accessibility');
|
||||
|
||||
const writeFileAsync = util.promisify(fs.writeFile);
|
||||
|
||||
class Page extends EventEmitter {
|
||||
/**
|
||||
*
|
||||
* @param {!Puppeteer.JugglerSession} connection
|
||||
* @param {!Puppeteer.Target} target
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
*/
|
||||
static async create(session, target, defaultViewport) {
|
||||
const page = new Page(session, target);
|
||||
await Promise.all([
|
||||
session.send('Runtime.enable'),
|
||||
session.send('Network.enable'),
|
||||
session.send('Page.enable'),
|
||||
]);
|
||||
|
||||
if (defaultViewport)
|
||||
await page.setViewport(defaultViewport);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!PageSession} session
|
||||
* @param {!Puppeteer.Target} target
|
||||
*/
|
||||
constructor(session, target) {
|
||||
super();
|
||||
this._timeoutSettings = new TimeoutSettings();
|
||||
this._session = session;
|
||||
this._target = target;
|
||||
this._keyboard = new Keyboard(session);
|
||||
this._mouse = new Mouse(session, this._keyboard);
|
||||
this._touchscreen = new Touchscreen(session, this._keyboard, this._mouse);
|
||||
this._accessibility = new Accessibility(session);
|
||||
this._closed = false;
|
||||
/** @type {!Map<string, Function>} */
|
||||
this._pageBindings = new Map();
|
||||
this._networkManager = new NetworkManager(session);
|
||||
this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings);
|
||||
this._networkManager.setFrameManager(this._frameManager);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
|
||||
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
|
||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.Load, () => this.emit(Events.Page.Load)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameDetached, frame => this.emit(Events.Page.FrameDetached, frame)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameNavigated, frame => this.emit(Events.Page.FrameNavigated, frame)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.Request, request => this.emit(Events.Page.Request, request)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.Response, response => this.emit(Events.Page.Response, response)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.RequestFinished, request => this.emit(Events.Page.RequestFinished, request)),
|
||||
helper.addEventListener(this._networkManager, Events.NetworkManager.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)),
|
||||
];
|
||||
this._viewport = null;
|
||||
this._target._isClosedPromise.then(() => {
|
||||
this._closed = true;
|
||||
this._frameManager.dispose();
|
||||
this._networkManager.dispose();
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this.emit(Events.Page.Close);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} urls
|
||||
* @return {!Promise<!Array<Network.Cookie>>}
|
||||
*/
|
||||
async cookies(...urls) {
|
||||
const connection = Connection.fromSession(this._session);
|
||||
return (await connection.send('Browser.getCookies', {
|
||||
browserContextId: this._target._context._browserContextId,
|
||||
urls: urls.length ? urls : [this.url()]
|
||||
})).cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Protocol.Network.deleteCookiesParameters>} cookies
|
||||
*/
|
||||
async deleteCookie(...cookies) {
|
||||
const pageURL = this.url();
|
||||
const items = [];
|
||||
for (const cookie of cookies) {
|
||||
const item = {
|
||||
url: cookie.url,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path,
|
||||
name: cookie.name,
|
||||
};
|
||||
if (!item.url && pageURL.startsWith('http'))
|
||||
item.url = pageURL;
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
const connection = Connection.fromSession(this._session);
|
||||
await connection.send('Browser.deleteCookies', {
|
||||
browserContextId: this._target._context._browserContextId,
|
||||
cookies: items,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Network.CookieParam>} cookies
|
||||
*/
|
||||
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;
|
||||
assert(item.url !== 'about:blank', `Blank page can not have cookie "${item.name}"`);
|
||||
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) {
|
||||
const connection = Connection.fromSession(this._session);
|
||||
await connection.send('Browser.setCookies', {
|
||||
browserContextId: this._target._context._browserContextId,
|
||||
cookies: items
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setRequestInterception(enabled) {
|
||||
await this._networkManager.setRequestInterception(enabled);
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
await this._networkManager.setExtraHTTPHeaders(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} mediaType
|
||||
*/
|
||||
async emulateMedia(mediaType) {
|
||||
assert(mediaType === 'screen' || mediaType === 'print' || mediaType === null, 'Unsupported media type: ' + mediaType);
|
||||
await this._session.send('Page.setEmulatedMedia', {media: mediaType || ''});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} puppeteerFunction
|
||||
*/
|
||||
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.evaluationString(addPageBinding, name);
|
||||
await this._session.send('Page.addBinding', {name: name});
|
||||
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: expression});
|
||||
await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
|
||||
|
||||
function addPageBinding(bindingName) {
|
||||
const binding = window[bindingName];
|
||||
window[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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Runtime.bindingCalledPayload} event
|
||||
*/
|
||||
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.evaluationString(deliverResult, name, seq, result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error)
|
||||
expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
|
||||
else
|
||||
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
|
||||
}
|
||||
this._session.send('Runtime.evaluate', { expression, executionContextId: event.executionContextId }).catch(debugError);
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {*} result
|
||||
*/
|
||||
function deliverResult(name, seq, result) {
|
||||
window[name]['callbacks'].get(seq).resolve(result);
|
||||
window[name]['callbacks'].delete(seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {string} message
|
||||
* @param {string} stack
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} seq
|
||||
* @param {*} value
|
||||
*/
|
||||
function deliverErrorValue(name, seq, value) {
|
||||
window[name]['callbacks'].get(seq).reject(value);
|
||||
window[name]['callbacks'].delete(seq);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|Function)} urlOrPredicate
|
||||
* @param {!{timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.Request>}
|
||||
*/
|
||||
async waitForRequest(urlOrPredicate, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return helper.waitForEvent(this._networkManager, Events.NetworkManager.Request, request => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === request.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(request));
|
||||
return false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|Function)} urlOrPredicate
|
||||
* @param {!{timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.Response>}
|
||||
*/
|
||||
async waitForResponse(urlOrPredicate, options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return helper.waitForEvent(this._networkManager, Events.NetworkManager.Response, response => {
|
||||
if (helper.isString(urlOrPredicate))
|
||||
return (urlOrPredicate === response.url());
|
||||
if (typeof urlOrPredicate === 'function')
|
||||
return !!(urlOrPredicate(response));
|
||||
return false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setUserAgent(userAgent) {
|
||||
await this._session.send('Page.setUserAgent', {userAgent});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setJavaScriptEnabled(enabled) {
|
||||
await this._session.send('Page.setJavascriptEnabled', {enabled});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setCacheEnabled(enabled) {
|
||||
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{viewport: !Puppeteer.Viewport, userAgent: string}} options
|
||||
*/
|
||||
async emulate(options) {
|
||||
await Promise.all([
|
||||
this.setViewport(options.viewport),
|
||||
this.setUserAgent(options.userAgent),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {BrowserContext}
|
||||
*/
|
||||
browserContext() {
|
||||
return this._target.browserContext();
|
||||
}
|
||||
|
||||
_onUncaughtError(params) {
|
||||
const error = new Error(params.message);
|
||||
error.stack = params.stack;
|
||||
this.emit(Events.Page.PageError, error);
|
||||
}
|
||||
|
||||
viewport() {
|
||||
return this._viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Viewport} viewport
|
||||
*/
|
||||
async setViewport(viewport) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
isMobile = false,
|
||||
deviceScaleFactor = 1,
|
||||
hasTouch = false,
|
||||
isLandscape = false,
|
||||
} = viewport;
|
||||
await this._session.send('Page.setViewport', {
|
||||
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
|
||||
});
|
||||
const oldIsMobile = this._viewport ? this._viewport.isMobile : false;
|
||||
const oldHasTouch = this._viewport ? this._viewport.hasTouch : false;
|
||||
this._viewport = viewport;
|
||||
if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
|
||||
await this.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function()|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
*/
|
||||
async evaluateOnNewDocument(pageFunction, ...args) {
|
||||
const script = helper.evaluationString(pageFunction, ...args);
|
||||
await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script });
|
||||
}
|
||||
|
||||
browser() {
|
||||
return this._target.browser();
|
||||
}
|
||||
|
||||
target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._frameManager.mainFrame().url();
|
||||
}
|
||||
|
||||
frames() {
|
||||
return this._frameManager.frames();
|
||||
}
|
||||
|
||||
_onDialogOpened(params) {
|
||||
this.emit(Events.Page.Dialog, new Dialog(this._session, params));
|
||||
}
|
||||
|
||||
mainFrame() {
|
||||
return this._frameManager.mainFrame();
|
||||
}
|
||||
|
||||
get accessibility() {
|
||||
return this._accessibility;
|
||||
}
|
||||
|
||||
get keyboard(){
|
||||
return this._keyboard;
|
||||
}
|
||||
|
||||
get mouse(){
|
||||
return this._mouse;
|
||||
}
|
||||
|
||||
get touchscreen(){
|
||||
return this._touchscreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async waitForNavigation(options = {}) {
|
||||
return this._frameManager.mainFrame().waitForNavigation(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goto(url, options = {}) {
|
||||
return this._frameManager.mainFrame().goto(url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goBack(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.goBack', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async goForward(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.goForward', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}} options
|
||||
*/
|
||||
async reload(options = {}) {
|
||||
const {
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
waitUntil = ['load'],
|
||||
} = options;
|
||||
const frame = this._frameManager.mainFrame();
|
||||
const normalizedWaitUntil = normalizeWaitUntil(waitUntil);
|
||||
const {navigationId, navigationURL} = await this._session.send('Page.reload', {
|
||||
frameId: frame._frameId,
|
||||
});
|
||||
if (!navigationId)
|
||||
return null;
|
||||
|
||||
const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms');
|
||||
let timeoutCallback;
|
||||
const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError));
|
||||
const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null;
|
||||
|
||||
const watchDog = new NavigationWatchdog(this._session, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil);
|
||||
const error = await Promise.race([
|
||||
timeoutPromise,
|
||||
watchDog.promise(),
|
||||
]);
|
||||
watchDog.dispose();
|
||||
clearTimeout(timeoutId);
|
||||
if (error)
|
||||
throw error;
|
||||
return watchDog.navigationResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{fullPage?: boolean, clip?: {width: number, height: number, x: number, y: number}, encoding?: string, path?: string}} options
|
||||
* @return {Promise<string|Buffer>}
|
||||
*/
|
||||
async screenshot(options = {}) {
|
||||
const {data} = await this._session.send('Page.screenshot', {
|
||||
mimeType: getScreenshotMimeType(options),
|
||||
fullPage: options.fullPage,
|
||||
clip: processClip(options.clip),
|
||||
});
|
||||
const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64');
|
||||
if (options.path)
|
||||
await writeFileAsync(options.path, buffer);
|
||||
return buffer;
|
||||
|
||||
function processClip(clip) {
|
||||
if (!clip)
|
||||
return undefined;
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
async evaluate(pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, type?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
return await this._frameManager.mainFrame().addScriptTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{content?: string, path?: string, url?: string}} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
return await this._frameManager.mainFrame().addStyleTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: string, clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options = {}) {
|
||||
return await this._frameManager.mainFrame().click(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
tap(selector) {
|
||||
return this.mainFrame().tap(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
return await this._frameManager.mainFrame().type(selector, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
return await this._frameManager.mainFrame().focus(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
return await this._frameManager.mainFrame().hover(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
||||
* @param {!{polling?: string|number, timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
||||
return await this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async waitForFunction(pageFunction, options = {}, ...args) {
|
||||
return await this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async waitForSelector(selector, options = {}) {
|
||||
return await this._frameManager.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{timeout?: number, visible?: boolean, hidden?: boolean}=} options
|
||||
* @return {!Promise<!ElementHandle>}
|
||||
*/
|
||||
async waitForXPath(xpath, options = {}) {
|
||||
return await this._frameManager.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return await this._frameManager.mainFrame().title();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
return await this._frameManager.mainFrame().$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
return await this._frameManager.mainFrame().$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
return await this._frameManager.mainFrame().$x(expression);
|
||||
}
|
||||
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
return await this._frameManager.mainFrame().evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
async select(selector, ...values) {
|
||||
return await this._frameManager.mainFrame().select(selector, ...values);
|
||||
}
|
||||
|
||||
async close(options = {}) {
|
||||
const {
|
||||
runBeforeUnload = false,
|
||||
} = options;
|
||||
await this._session.send('Page.close', { runBeforeUnload });
|
||||
if (!runBeforeUnload)
|
||||
await this._target._isClosedPromise;
|
||||
}
|
||||
|
||||
async content() {
|
||||
return await this._frameManager.mainFrame().content();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
async setContent(html) {
|
||||
return await this._frameManager.mainFrame().setContent(html);
|
||||
}
|
||||
|
||||
_onConsole({type, args, executionContextId, location}) {
|
||||
const context = this._frameManager.executionContextById(executionContextId);
|
||||
this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => createHandle(context, arg)), location));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isClosed() {
|
||||
return this._closed;
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleMessage {
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {!Array<!JSHandle>} args
|
||||
*/
|
||||
constructor(type, args, location) {
|
||||
this._type = type;
|
||||
this._args = args;
|
||||
this._location = location;
|
||||
}
|
||||
|
||||
location() {
|
||||
return this._location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!JSHandle>}
|
||||
*/
|
||||
args() {
|
||||
return this._args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
text() {
|
||||
return this._args.map(arg => {
|
||||
if (arg._objectId)
|
||||
return arg.toString();
|
||||
return arg._deserializeValue(arg._protocolValue);
|
||||
}).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
function getScreenshotMimeType(options) {
|
||||
// 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) {
|
||||
if (options.type === 'png')
|
||||
return 'image/png';
|
||||
if (options.type === 'jpeg')
|
||||
return 'image/jpeg';
|
||||
throw new Error('Unknown options.type value: ' + options.type);
|
||||
}
|
||||
if (options.path) {
|
||||
const fileType = mime.getType(options.path);
|
||||
if (fileType === 'image/png' || fileType === 'image/jpeg')
|
||||
return fileType;
|
||||
throw new Error('Unsupported screnshot mime type: ' + fileType);
|
||||
}
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
module.exports = {Page, ConsoleMessage};
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {Launcher} = require('./Launcher.js');
|
||||
const {BrowserFetcher} = require('./BrowserFetcher.js');
|
||||
const Errors = require('./Errors');
|
||||
const DeviceDescriptors = require('./DeviceDescriptors');
|
||||
|
||||
class Puppeteer {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {string} preferredRevision
|
||||
*/
|
||||
constructor(projectRoot, preferredRevision) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision);
|
||||
}
|
||||
|
||||
async launch(options = {}) {
|
||||
return this._launcher.launch(options);
|
||||
}
|
||||
|
||||
async connect(options) {
|
||||
return this._launcher.connect(options);
|
||||
}
|
||||
|
||||
createBrowserFetcher(options) {
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
|
||||
executablePath() {
|
||||
return this._launcher.executablePath();
|
||||
}
|
||||
|
||||
defaultArgs(options) {
|
||||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get devices() {
|
||||
return DeviceDescriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get errors() {
|
||||
return Errors;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Puppeteer};
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
class TimeoutSettings {
|
||||
constructor() {
|
||||
this._defaultTimeout = null;
|
||||
this._defaultNavigationTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultTimeout(timeout) {
|
||||
this._defaultTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._defaultNavigationTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {TimeoutSettings};
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDefinition
|
||||
* @property {number=} keyCode
|
||||
* @property {number=} shiftKeyCode
|
||||
* @property {string=} key
|
||||
* @property {string=} shiftKey
|
||||
* @property {string=} code
|
||||
* @property {string=} text
|
||||
* @property {string=} shiftText
|
||||
* @property {number=} location
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object<string, KeyDefinition>}
|
||||
*/
|
||||
module.exports = {
|
||||
'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'}
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 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 WebSocket = require('ws');
|
||||
|
||||
/**
|
||||
* @implements {!Puppeteer.ConnectionTransport}
|
||||
*/
|
||||
class WebSocketTransport {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!WebSocketTransport>}
|
||||
*/
|
||||
static create(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url, [], { perMessageDeflate: false });
|
||||
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
|
||||
ws.addEventListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!WebSocket} ws
|
||||
*/
|
||||
constructor(ws) {
|
||||
this._ws = ws;
|
||||
this._dispatchQueue = new DispatchQueue(this);
|
||||
this._ws.addEventListener('message', event => {
|
||||
this._dispatchQueue.enqueue(event.data);
|
||||
});
|
||||
this._ws.addEventListener('close', event => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
send(message) {
|
||||
this._ws.send(message);
|
||||
}
|
||||
|
||||
close() {
|
||||
this._ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
// We want to dispatch all "message" events in separate tasks
|
||||
// to make sure all message-related promises are resolved first
|
||||
// before dispatching next message.
|
||||
//
|
||||
// We cannot just use setTimeout() in Node.js here like we would
|
||||
// do in Browser - see https://github.com/nodejs/node/issues/23773
|
||||
// Thus implement a dispatch queue that enforces new tasks manually.
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class DispatchQueue {
|
||||
constructor(transport) {
|
||||
this._transport = transport;
|
||||
|
||||
this._timeoutId = null;
|
||||
this._queue = [];
|
||||
this._dispatch = this._dispatch.bind(this);
|
||||
}
|
||||
|
||||
enqueue(message) {
|
||||
this._queue.push(message);
|
||||
if (!this._timeoutId)
|
||||
this._timeoutId = setTimeout(this._dispatch, 0);
|
||||
}
|
||||
|
||||
_dispatch() {
|
||||
const message = this._queue.shift();
|
||||
if (this._queue.length)
|
||||
this._timeoutId = setTimeout(this._dispatch, 0)
|
||||
else
|
||||
this._timeoutId = null;
|
||||
|
||||
if (this._transport.onmessage)
|
||||
this._transport.onmessage.call(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketTransport;
|
|
@ -0,0 +1,22 @@
|
|||
module.exports = {
|
||||
Accessibility: require('./Accessibility').Accessibility,
|
||||
Browser: require('./Browser').Browser,
|
||||
BrowserContext: require('./Browser').BrowserContext,
|
||||
BrowserFetcher: require('./BrowserFetcher').BrowserFetcher,
|
||||
ConsoleMessage: require('./Page').ConsoleMessage,
|
||||
Dialog: require('./Dialog').Dialog,
|
||||
ElementHandle: require('./JSHandle').ElementHandle,
|
||||
ExecutionContext: require('./ExecutionContext').ExecutionContext,
|
||||
Frame: require('./FrameManager').Frame,
|
||||
JSHandle: require('./JSHandle').JSHandle,
|
||||
Keyboard: require('./Input').Keyboard,
|
||||
Mouse: require('./Input').Mouse,
|
||||
Page: require('./Page').Page,
|
||||
Puppeteer: require('./Puppeteer').Puppeteer,
|
||||
Request: require('./NetworkManager').Request,
|
||||
Response: require('./NetworkManager').Response,
|
||||
SecurityDetails: require('./NetworkManager').SecurityDetails,
|
||||
Target: require('./Browser').Target,
|
||||
Touchscreen: require('./Input').Touchscreen,
|
||||
TimeoutError: require('./Errors').TimeoutError,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { Connection as RealConnection } from './Connection';
|
||||
import { Target as RealTarget } from './Browser';
|
||||
import * as child_process from 'child_process';
|
||||
declare global {
|
||||
module Puppeteer {
|
||||
|
||||
export interface ConnectionTransport {
|
||||
send(string);
|
||||
close();
|
||||
onmessage?: (message: string) => void,
|
||||
onclose?: () => void,
|
||||
}
|
||||
|
||||
export interface ChildProcess extends child_process.ChildProcess { }
|
||||
|
||||
export type Viewport = {
|
||||
width: number;
|
||||
height: number;
|
||||
deviceScaleFactor?: number;
|
||||
isMobile?: boolean;
|
||||
isLandscape?: boolean;
|
||||
hasTouch?: boolean;
|
||||
}
|
||||
|
||||
export class Connection extends RealConnection { }
|
||||
export class Target extends RealTarget { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
/**
|
||||
* 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 {TimeoutError} = require('./Errors');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Helper {
|
||||
/**
|
||||
* @param {!Object} classType
|
||||
*/
|
||||
static 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 = new Error();
|
||||
return method.call(this, ...args).catch(e => {
|
||||
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
||||
const clientStack = stack.substring(stack.indexOf('\n'));
|
||||
if (e instanceof Error && e.stack && !e.stack.includes(clientStack))
|
||||
e.stack += '\n -- ASYNC --\n' + stack;
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} fun
|
||||
* @param {!Array<*>} args
|
||||
* @return {string}
|
||||
*/
|
||||
static evaluationString(fun, ...args) {
|
||||
if (Helper.isString(fun)) {
|
||||
if (args.length !== 0)
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
return /** @type {string} */ (fun);
|
||||
}
|
||||
return `(${fun})(${args.map(serializeArgument).join(',')})`;
|
||||
|
||||
/**
|
||||
* @param {*} arg
|
||||
* @return {string}
|
||||
*/
|
||||
function serializeArgument(arg) {
|
||||
if (Object.is(arg, undefined))
|
||||
return 'undefined';
|
||||
return JSON.stringify(arg);
|
||||
}
|
||||
}
|
||||
|
||||
static promisify(nodeFunction) {
|
||||
function promisified(...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function callback(err, ...result) {
|
||||
if (err)
|
||||
return reject(err);
|
||||
if (result.length === 1)
|
||||
return resolve(result[0]);
|
||||
return resolve(result);
|
||||
}
|
||||
nodeFunction.call(null, ...args, callback);
|
||||
});
|
||||
}
|
||||
return promisified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Object} obj
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isNumber(obj) {
|
||||
return typeof obj === 'number' || obj instanceof Number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Object} obj
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isString(obj) {
|
||||
return typeof obj === 'string' || obj instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!NodeJS.EventEmitter} emitter
|
||||
* @param {(string|symbol)} eventName
|
||||
* @param {function(?)} handler
|
||||
* @return {{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}}
|
||||
*/
|
||||
static addEventListener(emitter, eventName, handler) {
|
||||
emitter.on(eventName, handler);
|
||||
return { emitter, eventName, handler };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?)}>} listeners
|
||||
*/
|
||||
static removeEventListeners(listeners) {
|
||||
for (const listener of listeners)
|
||||
listener.emitter.removeListener(listener.eventName, listener.handler);
|
||||
listeners.splice(0, listeners.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!NodeJS.EventEmitter} emitter
|
||||
* @param {(string|symbol)} eventName
|
||||
* @param {function} predicate
|
||||
* @return {!Promise}
|
||||
*/
|
||||
static waitForEvent(emitter, eventName, predicate, timeout) {
|
||||
let eventTimeout, resolveCallback, rejectCallback;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
resolveCallback = resolve;
|
||||
rejectCallback = reject;
|
||||
});
|
||||
const listener = Helper.addEventListener(emitter, eventName, event => {
|
||||
if (!predicate(event))
|
||||
return;
|
||||
cleanup();
|
||||
resolveCallback(event);
|
||||
});
|
||||
if (timeout) {
|
||||
eventTimeout = setTimeout(() => {
|
||||
cleanup();
|
||||
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
|
||||
}, timeout);
|
||||
}
|
||||
function cleanup() {
|
||||
Helper.removeEventListeners([listener]);
|
||||
clearTimeout(eventTimeout);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {!Promise<T>} promise
|
||||
* @param {string} taskName
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<T>}
|
||||
*/
|
||||
static async waitWithTimeout(promise, taskName, timeout) {
|
||||
let reject;
|
||||
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
|
||||
const timeoutPromise = new Promise((resolve, x) => reject = x);
|
||||
const timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, errorText) {
|
||||
if (!condition)
|
||||
throw new Error(errorText);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
helper: Helper,
|
||||
debugError: require('debug')(`puppeteer:error`),
|
||||
assert,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
// Any comment. You must start the file with a single-line comment!
|
||||
pref("general.config.filename", "puppeteer.cfg");
|
||||
pref("general.config.obscure_value", 0);
|
|
@ -0,0 +1,59 @@
|
|||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Install browser preferences after downloading and unpacking
|
||||
// firefox instances.
|
||||
// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration
|
||||
async function installFirefoxPreferences(executablePath) {
|
||||
const firefoxFolder = path.dirname(executablePath);
|
||||
const {helper} = require('../lib/helper');
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
|
||||
let prefPath = '';
|
||||
let configPath = '';
|
||||
if (os.platform() === 'darwin') {
|
||||
prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref');
|
||||
configPath = path.join(firefoxFolder, '..', 'Resources');
|
||||
} else if (os.platform() === 'linux') {
|
||||
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults')))
|
||||
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults'));
|
||||
if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences')))
|
||||
await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'));
|
||||
prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences');
|
||||
configPath = firefoxFolder;
|
||||
} else if (os.platform() === 'win32') {
|
||||
prefPath = path.join(firefoxFolder, 'defaults', 'pref');
|
||||
configPath = firefoxFolder;
|
||||
} else {
|
||||
throw new Error('Unsupported platform: ' + os.platform());
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
copyFile({
|
||||
from: path.join(__dirname, '00-puppeteer-prefs.js'),
|
||||
to: path.join(prefPath, '00-puppeteer-prefs.js'),
|
||||
}),
|
||||
copyFile({
|
||||
from: path.join(__dirname, 'puppeteer.cfg'),
|
||||
to: path.join(configPath, 'puppeteer.cfg'),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function copyFile({from, to}) {
|
||||
var rd = fs.createReadStream(from);
|
||||
var wr = fs.createWriteStream(to);
|
||||
return new Promise(function(resolve, reject) {
|
||||
rd.on('error', reject);
|
||||
wr.on('error', reject);
|
||||
wr.on('finish', resolve);
|
||||
rd.pipe(wr);
|
||||
}).catch(function(error) {
|
||||
rd.destroy();
|
||||
wr.end();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = installFirefoxPreferences;
|
|
@ -0,0 +1,212 @@
|
|||
// Any comment. You must start the file with a comment!
|
||||
|
||||
// Make sure Shield doesn't hit the network.
|
||||
// pref("app.normandy.api_url", "");
|
||||
pref("app.normandy.enabled", false);
|
||||
|
||||
// Disable updater
|
||||
pref("app.update.enabled", false);
|
||||
// make absolutely sure it is really off
|
||||
pref("app.update.auto", false);
|
||||
pref("app.update.mode", 0);
|
||||
pref("app.update.service.enabled", false);
|
||||
|
||||
// Dislabe newtabpage
|
||||
pref("browser.startup.homepage", 'about:blank');
|
||||
pref("browser.newtabpage.enabled", false);
|
||||
// Disable topstories
|
||||
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||
|
||||
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
|
||||
// This doesn't affect Puppeteer operations, but spams console with a lot of
|
||||
// unpleasant errors.
|
||||
// (bug 1424372)
|
||||
pref("devtools.jsonview.enabled", false);
|
||||
|
||||
// Increase the APZ content response timeout in tests to 1 minute.
|
||||
// This is to accommodate the fact that test environments tends to be
|
||||
// slower than production environments (with the b2g emulator being
|
||||
// the slowest of them all), resulting in the production timeout value
|
||||
// sometimes being exceeded and causing false-positive test failures.
|
||||
//
|
||||
// (bug 1176798, bug 1177018, bug 1210465)
|
||||
pref("apz.content_response_timeout", 60000);
|
||||
|
||||
// Allow creating files in content process - required for
|
||||
// |Page.setFileInputFiles| protocol method.
|
||||
pref("dom.file.createInChild", true);
|
||||
|
||||
// Indicate that the download panel has been shown once so that
|
||||
// whichever download test runs first doesn't show the popup
|
||||
// inconsistently.
|
||||
pref("browser.download.panel.shown", true);
|
||||
|
||||
// Background thumbnails in particular cause grief, and disabling
|
||||
// thumbnails in general cannot hurt
|
||||
pref("browser.pagethumbnails.capturing_disabled", true);
|
||||
|
||||
// Disable safebrowsing components.
|
||||
pref("browser.safebrowsing.blockedURIs.enabled", false);
|
||||
pref("browser.safebrowsing.downloads.enabled", false);
|
||||
pref("browser.safebrowsing.passwords.enabled", false);
|
||||
pref("browser.safebrowsing.malware.enabled", false);
|
||||
pref("browser.safebrowsing.phishing.enabled", false);
|
||||
|
||||
// Disable updates to search engines.
|
||||
pref("browser.search.update", false);
|
||||
|
||||
// Do not restore the last open set of tabs if the browser has crashed
|
||||
pref("browser.sessionstore.resume_from_crash", false);
|
||||
|
||||
// Don't check for the default web browser during startup.
|
||||
pref("browser.shell.checkDefaultBrowser", false);
|
||||
|
||||
// Do not redirect user when a milstone upgrade of Firefox is detected
|
||||
pref("browser.startup.homepage_override.mstone", "ignore");
|
||||
|
||||
// Disable browser animations (tabs, fullscreen, sliding alerts)
|
||||
pref("toolkit.cosmeticAnimations.enabled", false);
|
||||
|
||||
// Close the window when the last tab gets closed
|
||||
pref("browser.tabs.closeWindowWithLastTab", true);
|
||||
|
||||
// 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
|
||||
pref("browser.tabs.disableBackgroundZombification", false);
|
||||
|
||||
// Do not warn when closing all open tabs
|
||||
pref("browser.tabs.warnOnClose", false);
|
||||
|
||||
// Do not warn when closing all other open tabs
|
||||
pref("browser.tabs.warnOnCloseOtherTabs", false);
|
||||
|
||||
// Do not warn when multiple tabs will be opened
|
||||
pref("browser.tabs.warnOnOpen", false);
|
||||
|
||||
// Disable first run splash page on Windows 10
|
||||
pref("browser.usedOnWindows10.introURL", "");
|
||||
|
||||
// Disable the UI tour.
|
||||
//
|
||||
// Should be set in profile.
|
||||
pref("browser.uitour.enabled", false);
|
||||
|
||||
// Turn off search suggestions in the location bar so as not to trigger
|
||||
// network connections.
|
||||
pref("browser.urlbar.suggest.searches", false);
|
||||
|
||||
// Do not warn on quitting Firefox
|
||||
pref("browser.warnOnQuit", false);
|
||||
|
||||
// Do not show datareporting policy notifications which can
|
||||
// interfere with tests
|
||||
pref(
|
||||
"datareporting.healthreport.documentServerURI",
|
||||
"http://%(server)s/dummy/healthreport/",
|
||||
);
|
||||
pref("datareporting.healthreport.logging.consoleEnabled", false);
|
||||
pref("datareporting.healthreport.service.enabled", false);
|
||||
pref("datareporting.healthreport.service.firstRun", false);
|
||||
pref("datareporting.healthreport.uploadEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
|
||||
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
||||
|
||||
// Automatically unload beforeunload alerts
|
||||
pref("dom.disable_beforeunload", false);
|
||||
|
||||
// Disable popup-blocker
|
||||
pref("dom.disable_open_during_load", false);
|
||||
|
||||
// Disable the ProcessHangMonitor
|
||||
pref("dom.ipc.reportProcessHangs", false);
|
||||
|
||||
// Disable slow script dialogues
|
||||
pref("dom.max_chrome_script_run_time", 0);
|
||||
pref("dom.max_script_run_time", 0);
|
||||
|
||||
// Only load extensions from the application and user profile
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
pref("extensions.autoDisableScopes", 0);
|
||||
pref("extensions.enabledScopes", 5);
|
||||
|
||||
// Disable metadata caching for installed add-ons by default
|
||||
pref("extensions.getAddons.cache.enabled", false);
|
||||
|
||||
// Disable installing any distribution extensions or add-ons.
|
||||
pref("extensions.installDistroAddons", false);
|
||||
|
||||
// Turn off extension updates so they do not bother tests
|
||||
pref("extensions.update.enabled", false);
|
||||
pref("extensions.update.notifyUser", false);
|
||||
|
||||
// Make sure opening about:addons will not hit the network
|
||||
pref(
|
||||
"extensions.webservice.discoverURL",
|
||||
"http://%(server)s/dummy/discoveryURL",
|
||||
);
|
||||
|
||||
pref("extensions.screenshots.disabled", true);
|
||||
pref("extensions.screenshots.upload-disabled", true);
|
||||
|
||||
// Allow the application to have focus even it runs in the background
|
||||
pref("focusmanager.testmode", true);
|
||||
|
||||
// Disable useragent updates
|
||||
pref("general.useragent.updates.enabled", false);
|
||||
|
||||
// Always use network provider for geolocation tests so we bypass the
|
||||
// macOS dialog raised by the corelocation provider
|
||||
pref("geo.provider.testing", true);
|
||||
|
||||
// Do not scan Wifi
|
||||
pref("geo.wifi.scan", false);
|
||||
|
||||
// Show chrome errors and warnings in the error console
|
||||
pref("javascript.options.showInConsole", true);
|
||||
|
||||
// Do not prompt with long usernames or passwords in URLs
|
||||
pref("network.http.phishy-userpass-length", 255);
|
||||
|
||||
// Do not prompt for temporary redirects
|
||||
pref("network.http.prompt-temp-redirect", false);
|
||||
|
||||
// Disable speculative connections so they are not reported as leaking
|
||||
// when they are hanging around
|
||||
pref("network.http.speculative-parallel-limit", 0);
|
||||
|
||||
// Do not automatically switch between offline and online
|
||||
pref("network.manage-offline-status", false);
|
||||
|
||||
// Make sure SNTP requests do not hit the network
|
||||
pref("network.sntp.pools", "%(server)s");
|
||||
|
||||
// Local documents have access to all other local documents,
|
||||
// including directory listings
|
||||
pref("security.fileuri.strict_origin_policy", false);
|
||||
|
||||
// Tests do not wait for the notification button security delay
|
||||
pref("security.notification_enable_delay", 0);
|
||||
|
||||
// Ensure blocklist updates do not hit the network
|
||||
pref("services.settings.server", "http://%(server)s/dummy/blocklist/");
|
||||
|
||||
// Do not automatically fill sign-in forms with known usernames and
|
||||
// passwords
|
||||
pref("signon.autofillForms", false);
|
||||
|
||||
// Disable password capture, so that tests that include forms are not
|
||||
// influenced by the presence of the persistent doorhanger notification
|
||||
pref("signon.rememberSignons", false);
|
||||
|
||||
// Disable first-run welcome page
|
||||
pref("startup.homepage_welcome_url", "about:blank");
|
||||
pref("startup.homepage_welcome_url.additional", "");
|
||||
|
||||
// Prevent starting into safe mode after application crashes
|
||||
pref("toolkit.startup.max_resumed_crashes", -1);
|
||||
lockPref("toolkit.crashreporter.enabled", false);
|
||||
|
||||
// Disable crash reporter.
|
||||
Components.classes["@mozilla.org/toolkit/crash-reporter;1"].getService(Components.interfaces.nsICrashReporter).submitReports = false;
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "puppeteer-firefox",
|
||||
"version": "0.5.0",
|
||||
"description": "Puppeteer API for Firefox",
|
||||
"main": "index.js",
|
||||
"repository": "github:GoogleChrome/puppeteer",
|
||||
"homepage": "https://github.com/GoogleChrome/puppeteer/tree/master/experimental/puppeteer-firefox",
|
||||
"engines": {
|
||||
"node": ">=8.9.4"
|
||||
},
|
||||
"puppeteer": {
|
||||
"firefox_revision": "765beffcf39dc68cb2005b2b5343e283e26df7a3"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "node install.js",
|
||||
"tsc": "tsc -p ."
|
||||
},
|
||||
"author": "The Chromium Authors",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0",
|
||||
"extract-zip": "^1.6.6",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"mime": "^2.0.3",
|
||||
"progress": "^2.0.1",
|
||||
"proxy-from-env": "^1.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"ws": "^6.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"target": "es2017",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let asyncawait = true;
|
||||
try {
|
||||
new Function('async function test(){await 1}');
|
||||
} catch (error) {
|
||||
asyncawait = false;
|
||||
}
|
||||
|
||||
if (asyncawait) {
|
||||
const {helper} = require('./lib/helper');
|
||||
const api = require('./lib/api');
|
||||
for (const className in api) {
|
||||
// Puppeteer-web excludes certain classes from bundle, e.g. BrowserFetcher.
|
||||
if (typeof api[className] === 'function')
|
||||
helper.installAsyncStackHooks(api[className]);
|
||||
}
|
||||
}
|
||||
|
||||
// If node does not support async await, use the compiled version.
|
||||
const Puppeteer = asyncawait ? require('./lib/Puppeteer') : require('./node6/lib/Puppeteer');
|
||||
const packageJson = require('./package.json');
|
||||
const preferredRevision = packageJson.puppeteer.chromium_revision;
|
||||
const isPuppeteerCore = packageJson.name === 'puppeteer-core';
|
||||
|
||||
module.exports = new Puppeteer(__dirname, preferredRevision, isPuppeteerCore);
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// puppeteer-core should not install anything.
|
||||
if (require('./package.json').name === 'puppeteer-core')
|
||||
return;
|
||||
|
||||
buildNode6IfNecessary();
|
||||
|
||||
if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) {
|
||||
logPolitely('**INFO** Skipping Chromium 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 Chromium 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 Chromium download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.');
|
||||
return;
|
||||
}
|
||||
|
||||
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 browserFetcher = puppeteer.createBrowserFetcher({ host: downloadHost });
|
||||
|
||||
const revision = 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;
|
||||
|
||||
const revisionInfo = browserFetcher.revisionInfo(revision);
|
||||
|
||||
// Do nothing if the revision is already downloaded.
|
||||
if (revisionInfo.local) {
|
||||
generateProtocolTypesIfNecessary(false /* updated */);
|
||||
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;
|
||||
|
||||
browserFetcher.download(revisionInfo.revision, onProgress)
|
||||
.then(() => browserFetcher.localRevisions())
|
||||
.then(onSuccess)
|
||||
.catch(onError);
|
||||
|
||||
/**
|
||||
* @param {!Array<string>}
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function onSuccess(localRevisions) {
|
||||
logPolitely('Chromium downloaded to ' + revisionInfo.folderPath);
|
||||
localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
|
||||
// Remove previous chromium revisions.
|
||||
const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));
|
||||
return Promise.all([...cleanupOldVersions, generateProtocolTypesIfNecessary(true /* updated */)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
function onError(error) {
|
||||
console.error(`ERROR: Failed to download Chromium r${revision}! Set "PUPPETEER_SKIP_CHROMIUM_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 Chromium r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
width: 20,
|
||||
total: totalBytes,
|
||||
});
|
||||
}
|
||||
const delta = downloadedBytes - lastDownloadedBytes;
|
||||
lastDownloadedBytes = downloadedBytes;
|
||||
progressBar.tick(delta);
|
||||
}
|
||||
|
||||
function toMegabytes(bytes) {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return `${Math.round(mb * 10) / 10} Mb`;
|
||||
}
|
||||
|
||||
function buildNode6IfNecessary() {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// if this package is installed from NPM, then it already has up-to-date node6
|
||||
// folder.
|
||||
if (!fs.existsSync(path.join('utils', 'node6-transform')))
|
||||
return;
|
||||
// if async/await is supported, then node6 is not needed.
|
||||
if (supportsAsyncAwait())
|
||||
return;
|
||||
// Re-build node6/ folder.
|
||||
logPolitely('Building Puppeteer for Node 6');
|
||||
require(path.join(__dirname, 'utils', 'node6-transform'));
|
||||
}
|
||||
|
||||
function supportsAsyncAwait() {
|
||||
try {
|
||||
new Function('async function test(){await 1}');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateProtocolTypesIfNecessary(updated) {
|
||||
if (!supportsAsyncAwait())
|
||||
return;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
if (!fs.existsSync(path.join(__dirname, 'utils', 'protocol-types-generator')))
|
||||
return;
|
||||
if (!updated && fs.existsSync(path.join(__dirname, 'lib', 'protocol.d.ts')))
|
||||
return;
|
||||
return require('./utils/protocol-types-generator');
|
||||
}
|
||||
|
||||
function logPolitely(toBeLogged) {
|
||||
const logLevel = process.env.npm_config_loglevel;
|
||||
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
|
||||
|
||||
if (!logLevelDisplay)
|
||||
console.log(toBeLogged);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
"extends": "../.eslintrc.js",
|
||||
/**
|
||||
* ESLint rules
|
||||
*
|
||||
* All available rules: http://eslint.org/docs/rules/
|
||||
*
|
||||
* Rules take the following form:
|
||||
* "rule-name", [severity, { opts }]
|
||||
* Severity: 2 == error, 1 == warning, 0 == off.
|
||||
*/
|
||||
"rules": {
|
||||
"no-console": [2, { "allow": ["warn", "error", "assert", "timeStamp", "time", "timeEnd"] }],
|
||||
"no-debugger": 0,
|
||||
}
|
||||
};
|
|
@ -0,0 +1,422 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SerializedAXNode
|
||||
* @property {string} role
|
||||
*
|
||||
* @property {string=} name
|
||||
* @property {string|number=} value
|
||||
* @property {string=} description
|
||||
*
|
||||
* @property {string=} keyshortcuts
|
||||
* @property {string=} roledescription
|
||||
* @property {string=} valuetext
|
||||
*
|
||||
* @property {boolean=} disabled
|
||||
* @property {boolean=} expanded
|
||||
* @property {boolean=} focused
|
||||
* @property {boolean=} modal
|
||||
* @property {boolean=} multiline
|
||||
* @property {boolean=} multiselectable
|
||||
* @property {boolean=} readonly
|
||||
* @property {boolean=} required
|
||||
* @property {boolean=} selected
|
||||
*
|
||||
* @property {boolean|"mixed"=} checked
|
||||
* @property {boolean|"mixed"=} pressed
|
||||
*
|
||||
* @property {number=} level
|
||||
* @property {number=} valuemin
|
||||
* @property {number=} valuemax
|
||||
*
|
||||
* @property {string=} autocomplete
|
||||
* @property {string=} haspopup
|
||||
* @property {string=} invalid
|
||||
* @property {string=} orientation
|
||||
*
|
||||
* @property {Array<SerializedAXNode>=} children
|
||||
*/
|
||||
|
||||
class Accessibility {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{interestingOnly?: boolean, root?: ?Puppeteer.ElementHandle}=} options
|
||||
* @return {!Promise<!SerializedAXNode>}
|
||||
*/
|
||||
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 serializeTree(needle)[0];
|
||||
|
||||
/** @type {!Set<!AXNode>} */
|
||||
const interestingNodes = new Set();
|
||||
collectInterestingNodes(interestingNodes, defaultRoot, false);
|
||||
if (!interestingNodes.has(needle))
|
||||
return null;
|
||||
return serializeTree(needle, interestingNodes)[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Set<!AXNode>} collection
|
||||
* @param {!AXNode} node
|
||||
* @param {boolean} insideControl
|
||||
*/
|
||||
function 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)
|
||||
collectInterestingNodes(collection, child, insideControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!AXNode} node
|
||||
* @param {!Set<!AXNode>=} whitelistedNodes
|
||||
* @return {!Array<!SerializedAXNode>}
|
||||
*/
|
||||
function serializeTree(node, whitelistedNodes) {
|
||||
/** @type {!Array<!SerializedAXNode>} */
|
||||
const children = [];
|
||||
for (const child of node._children)
|
||||
children.push(...serializeTree(child, whitelistedNodes));
|
||||
|
||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
||||
return children;
|
||||
|
||||
const serializedNode = node.serialize();
|
||||
if (children.length)
|
||||
serializedNode.children = children;
|
||||
return [serializedNode];
|
||||
}
|
||||
|
||||
|
||||
class AXNode {
|
||||
/**
|
||||
* @param {!Protocol.Accessibility.AXNode} payload
|
||||
*/
|
||||
constructor(payload) {
|
||||
this._payload = payload;
|
||||
|
||||
/** @type {!Array<!AXNode>} */
|
||||
this._children = [];
|
||||
|
||||
this._richlyEditable = false;
|
||||
this._editable = false;
|
||||
this._focusable = false;
|
||||
this._expanded = false;
|
||||
this._name = this._payload.name ? this._payload.name.value : '';
|
||||
this._role = this._payload.role ? this._payload.role.value : 'Unknown';
|
||||
this._cachedHasFocusableChild;
|
||||
|
||||
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 === 'expanded')
|
||||
this._expanded = property.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isPlainTextField() {
|
||||
if (this._richlyEditable)
|
||||
return false;
|
||||
if (this._editable)
|
||||
return true;
|
||||
return this._role === 'textbox' || this._role === 'ComboBox' || this._role === 'searchbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isTextOnlyObject() {
|
||||
const role = this._role;
|
||||
return (role === 'LineBreak' || role === 'text' ||
|
||||
role === 'InlineTextBox');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(AXNode):boolean} predicate
|
||||
* @return {?AXNode}
|
||||
*/
|
||||
find(predicate) {
|
||||
if (predicate(this))
|
||||
return this;
|
||||
for (const child of this._children) {
|
||||
const result = child.find(predicate);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} insideControl
|
||||
* @return {boolean}
|
||||
*/
|
||||
isInteresting(insideControl) {
|
||||
const role = this._role;
|
||||
if (role === 'Ignored')
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!SerializedAXNode}
|
||||
*/
|
||||
serialize() {
|
||||
/** @type {!Map<string, number|string|boolean>} */
|
||||
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);
|
||||
|
||||
/** @type {SerializedAXNode} */
|
||||
const node = {
|
||||
role: this._role
|
||||
};
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const userStringProperties = [
|
||||
'name',
|
||||
'value',
|
||||
'description',
|
||||
'keyshortcuts',
|
||||
'roledescription',
|
||||
'valuetext',
|
||||
];
|
||||
for (const userStringProperty of userStringProperties) {
|
||||
if (!properties.has(userStringProperty))
|
||||
continue;
|
||||
node[userStringProperty] = properties.get(userStringProperty);
|
||||
}
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const booleanProperties = [
|
||||
'disabled',
|
||||
'expanded',
|
||||
'focused',
|
||||
'modal',
|
||||
'multiline',
|
||||
'multiselectable',
|
||||
'readonly',
|
||||
'required',
|
||||
'selected',
|
||||
];
|
||||
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 = properties.get(booleanProperty);
|
||||
if (!value)
|
||||
continue;
|
||||
node[booleanProperty] = value;
|
||||
}
|
||||
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
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;
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const numericalProperties = [
|
||||
'level',
|
||||
'valuemax',
|
||||
'valuemin',
|
||||
];
|
||||
for (const numericalProperty of numericalProperties) {
|
||||
if (!properties.has(numericalProperty))
|
||||
continue;
|
||||
node[numericalProperty] = properties.get(numericalProperty);
|
||||
}
|
||||
/** @type {!Array<keyof SerializedAXNode>} */
|
||||
const tokenProperties = [
|
||||
'autocomplete',
|
||||
'haspopup',
|
||||
'invalid',
|
||||
'orientation',
|
||||
];
|
||||
for (const tokenProperty of tokenProperties) {
|
||||
const value = properties.get(tokenProperty);
|
||||
if (!value || value === 'false')
|
||||
continue;
|
||||
node[tokenProperty] = value;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<!Protocol.Accessibility.AXNode>} payloads
|
||||
* @return {!AXNode}
|
||||
*/
|
||||
static createTree(payloads) {
|
||||
/** @type {!Map<string, !AXNode>} */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Accessibility};
|
|
@ -0,0 +1,383 @@
|
|||
/**
|
||||
* 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, assert } = require('./helper');
|
||||
const {Target} = require('./Target');
|
||||
const EventEmitter = require('events');
|
||||
const {TaskQueue} = require('./TaskQueue');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
class Browser extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Array<string>} contextIds
|
||||
* @param {boolean} ignoreHTTPSErrors
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {function()=} closeCallback
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Array<string>} contextIds
|
||||
* @param {boolean} ignoreHTTPSErrors
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {?Puppeteer.ChildProcess} process
|
||||
* @param {(function():Promise)=} closeCallback
|
||||
*/
|
||||
constructor(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback) {
|
||||
super();
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._process = process;
|
||||
this._screenshotTaskQueue = new TaskQueue();
|
||||
this._connection = connection;
|
||||
this._closeCallback = closeCallback || new Function();
|
||||
|
||||
this._defaultContext = new BrowserContext(this._connection, this, null);
|
||||
/** @type {Map<string, BrowserContext>} */
|
||||
this._contexts = new Map();
|
||||
for (const contextId of contextIds)
|
||||
this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));
|
||||
|
||||
/** @type {Map<string, Target>} */
|
||||
this._targets = new Map();
|
||||
this._connection.on(Events.Connection.Disconnected, () => this.emit(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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.ChildProcess}
|
||||
*/
|
||||
process() {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!BrowserContext>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!BrowserContext>}
|
||||
*/
|
||||
browserContexts() {
|
||||
return [this._defaultContext, ...Array.from(this._contexts.values())];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!BrowserContext}
|
||||
*/
|
||||
defaultBrowserContext() {
|
||||
return this._defaultContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} contextId
|
||||
*/
|
||||
async _disposeContext(contextId) {
|
||||
await this._connection.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
|
||||
this._contexts.delete(contextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Target.targetCreatedPayload} event
|
||||
*/
|
||||
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(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue);
|
||||
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.Browser.TargetCreated, target);
|
||||
context.emit(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.Browser.TargetDestroyed, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetDestroyed, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Target.targetInfoChangedPayload} event
|
||||
*/
|
||||
_targetInfoChanged(event) {
|
||||
const target = this._targets.get(event.targetInfo.targetId);
|
||||
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.Browser.TargetChanged, target);
|
||||
target.browserContext().emit(Events.BrowserContext.TargetChanged, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
wsEndpoint() {
|
||||
return this._connection.url();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Puppeteer.Page>}
|
||||
*/
|
||||
async newPage() {
|
||||
return this._defaultContext.newPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?string} contextId
|
||||
* @return {!Promise<!Puppeteer.Page>}
|
||||
*/
|
||||
async _createPageInContext(contextId) {
|
||||
const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined});
|
||||
const target = await this._targets.get(targetId);
|
||||
assert(await target._initializedPromise, 'Failed to create target for page');
|
||||
const page = await target.page();
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!Target>}
|
||||
*/
|
||||
targets() {
|
||||
return Array.from(this._targets.values()).filter(target => target._isInitialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Target}
|
||||
*/
|
||||
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.Browser.TargetCreated, check);
|
||||
this.on(Events.Browser.TargetChanged, check);
|
||||
try {
|
||||
if (!timeout)
|
||||
return await targetPromise;
|
||||
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
|
||||
} finally {
|
||||
this.removeListener(Events.Browser.TargetCreated, check);
|
||||
this.removeListener(Events.Browser.TargetChanged, check);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Target} target
|
||||
*/
|
||||
function check(target) {
|
||||
if (predicate(target))
|
||||
resolve(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!Puppeteer.Page>>}
|
||||
*/
|
||||
async pages() {
|
||||
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
|
||||
// Flatten array.
|
||||
return contextPages.reduce((acc, x) => acc.concat(x), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async version() {
|
||||
const version = await this._getVersion();
|
||||
return version.product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async userAgent() {
|
||||
const version = await this._getVersion();
|
||||
return version.userAgent;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._closeCallback.call(null);
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._connection.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return !this._connection._closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Object>}
|
||||
*/
|
||||
_getVersion() {
|
||||
return this._connection.send('Browser.getVersion');
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserContext extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.Connection} connection
|
||||
* @param {!Browser} browser
|
||||
* @param {?string} contextId
|
||||
*/
|
||||
constructor(connection, browser, contextId) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._browser = browser;
|
||||
this._id = contextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!Target>} target
|
||||
*/
|
||||
targets() {
|
||||
return this._browser.targets().filter(target => target.browserContext() === this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(!Target):boolean} predicate
|
||||
* @param {{timeout?: number}=} options
|
||||
* @return {!Promise<!Target>}
|
||||
*/
|
||||
waitForTarget(predicate, options) {
|
||||
return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!Puppeteer.Page>>}
|
||||
*/
|
||||
async pages() {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => target.type() === 'page')
|
||||
.map(target => target.page())
|
||||
);
|
||||
return pages.filter(page => !!page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isIncognito() {
|
||||
return !!this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} origin
|
||||
* @param {!Array<string>} permissions
|
||||
*/
|
||||
async overridePermissions(origin, permissions) {
|
||||
const webPermissionToProtocol = new Map([
|
||||
['geolocation', 'geolocation'],
|
||||
['midi', 'midi'],
|
||||
['notifications', 'notifications'],
|
||||
['push', 'push'],
|
||||
['camera', 'videoCapture'],
|
||||
['microphone', 'audioCapture'],
|
||||
['background-sync', 'backgroundSync'],
|
||||
['ambient-light-sensor', 'sensors'],
|
||||
['accelerometer', 'sensors'],
|
||||
['gyroscope', 'sensors'],
|
||||
['magnetometer', 'sensors'],
|
||||
['accessibility-events', 'accessibilityEvents'],
|
||||
['clipboard-read', 'clipboardRead'],
|
||||
['clipboard-write', 'clipboardWrite'],
|
||||
['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});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Puppeteer.Page>}
|
||||
*/
|
||||
newPage() {
|
||||
return this._browser._createPageInContext(this._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Browser}
|
||||
*/
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
async close() {
|
||||
assert(this._id, 'Non-incognito profiles cannot be closed!');
|
||||
await this._browser._disposeContext(this._id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Browser, BrowserContext};
|
|
@ -0,0 +1,311 @@
|
|||
/**
|
||||
* 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 fs = require('fs');
|
||||
const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
const util = require('util');
|
||||
const URL = require('url');
|
||||
const {helper, assert} = require('./helper');
|
||||
const removeRecursive = require('rimraf');
|
||||
// @ts-ignore
|
||||
const ProxyAgent = require('https-proxy-agent');
|
||||
// @ts-ignore
|
||||
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
|
||||
|
||||
const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
|
||||
|
||||
const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
|
||||
const downloadURLs = {
|
||||
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',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} platform
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function archiveName(platform, revision) {
|
||||
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';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} platform
|
||||
* @param {string} host
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
function downloadURL(platform, host, revision) {
|
||||
return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision));
|
||||
}
|
||||
|
||||
const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
||||
const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
||||
|
||||
function existsAsync(filePath) {
|
||||
let fulfill = null;
|
||||
const promise = new Promise(x => fulfill = x);
|
||||
fs.access(filePath, err => fulfill(!err));
|
||||
return promise;
|
||||
}
|
||||
|
||||
class BrowserFetcher {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {!BrowserFetcher.Options=} options
|
||||
*/
|
||||
constructor(projectRoot, options = {}) {
|
||||
this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium');
|
||||
this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
||||
this._platform = options.platform || '';
|
||||
if (!this._platform) {
|
||||
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';
|
||||
assert(this._platform, 'Unsupported platform: ' + os.platform());
|
||||
}
|
||||
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!Promise<boolean>}
|
||||
*/
|
||||
canDownload(revision) {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
let resolve;
|
||||
const promise = new Promise(x => resolve = x);
|
||||
const request = httpRequest(url, 'HEAD', response => {
|
||||
resolve(response.statusCode === 200);
|
||||
});
|
||||
request.on('error', error => {
|
||||
console.error(error);
|
||||
resolve(false);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @param {?function(number, number):void} progressCallback
|
||||
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
|
||||
*/
|
||||
async download(revision, progressCallback) {
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
if (await existsAsync(folderPath))
|
||||
return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
try {
|
||||
await downloadFile(url, zipPath, progressCallback);
|
||||
await extractZip(zipPath, folderPath);
|
||||
} finally {
|
||||
if (await existsAsync(zipPath))
|
||||
await unlinkAsync(zipPath);
|
||||
}
|
||||
const revisionInfo = this.revisionInfo(revision);
|
||||
if (revisionInfo)
|
||||
await chmodAsync(revisionInfo.executablePath, 0o755);
|
||||
return revisionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
async localRevisions() {
|
||||
if (!await existsAsync(this._downloadsFolder))
|
||||
return [];
|
||||
const fileNames = await readdirAsync(this._downloadsFolder);
|
||||
return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
*/
|
||||
async remove(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
|
||||
await new Promise(fulfill => removeRecursive(folderPath, fulfill));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {!BrowserFetcher.RevisionInfo}
|
||||
*/
|
||||
revisionInfo(revision) {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._platform === 'mac')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
||||
else if (this._platform === 'linux')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome');
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + this._platform);
|
||||
const url = downloadURL(this._platform, this._downloadHost, revision);
|
||||
const local = fs.existsSync(folderPath);
|
||||
return {revision, executablePath, folderPath, local, url};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} revision
|
||||
* @return {string}
|
||||
*/
|
||||
_getFolderPath(revision) {
|
||||
return path.join(this._downloadsFolder, this._platform + '-' + revision);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrowserFetcher;
|
||||
|
||||
/**
|
||||
* @param {string} folderPath
|
||||
* @return {?{platform: string, revision: string}}
|
||||
*/
|
||||
function parseFolderPath(folderPath) {
|
||||
const name = path.basename(folderPath);
|
||||
const splits = name.split('-');
|
||||
if (splits.length !== 2)
|
||||
return null;
|
||||
const [platform, revision] = splits;
|
||||
if (!supportedPlatforms.includes(platform))
|
||||
return null;
|
||||
return {platform, revision};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} destinationPath
|
||||
* @param {?function(number, number):void} progressCallback
|
||||
* @return {!Promise}
|
||||
*/
|
||||
function downloadFile(url, destinationPath, progressCallback) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} zipPath
|
||||
* @param {string} folderPath
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
function extractZip(zipPath, folderPath) {
|
||||
return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
fulfill();
|
||||
}));
|
||||
}
|
||||
|
||||
function httpRequest(url, method, response) {
|
||||
/** @type {Object} */
|
||||
const options = URL.parse(url);
|
||||
options.method = method;
|
||||
|
||||
const proxyURL = getProxyForUrl(url);
|
||||
if (proxyURL) {
|
||||
/** @type {Object} */
|
||||
const parsedProxyURL = URL.parse(proxyURL);
|
||||
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
||||
|
||||
options.agent = new ProxyAgent(parsedProxyURL);
|
||||
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:' ?
|
||||
require('https').request(options, requestCallback) :
|
||||
require('http').request(options, requestCallback);
|
||||
request.end();
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.Options
|
||||
* @property {string=} platform
|
||||
* @property {string=} path
|
||||
* @property {string=} host
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrowserFetcher.RevisionInfo
|
||||
* @property {string} folderPath
|
||||
* @property {string} executablePath
|
||||
* @property {string} url
|
||||
* @property {boolean} local
|
||||
* @property {string} revision
|
||||
*/
|
|
@ -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.
|
||||
*/
|
||||
const {assert} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
const debugProtocol = require('debug')('puppeteer:protocol');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!Puppeteer.ConnectionTransport} transport
|
||||
* @param {number=} delay
|
||||
*/
|
||||
constructor(url, transport, delay = 0) {
|
||||
super();
|
||||
this._url = url;
|
||||
this._lastId = 0;
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._delay = delay;
|
||||
|
||||
this._transport = transport;
|
||||
this._transport.onmessage = this._onMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
/** @type {!Map<string, !CDPSession>}*/
|
||||
this._sessions = new Map();
|
||||
this._closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!CDPSession} session
|
||||
* @return {!Connection}
|
||||
*/
|
||||
static fromSession(session) {
|
||||
return session._connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionId
|
||||
* @return {?CDPSession}
|
||||
*/
|
||||
session(sessionId) {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
send(method, params = {}) {
|
||||
const id = this._rawSend({method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} message
|
||||
* @return {number}
|
||||
*/
|
||||
_rawSend(message) {
|
||||
const id = ++this._lastId;
|
||||
message = JSON.stringify(Object.assign({}, message, {id}));
|
||||
debugProtocol('SEND ► ' + message);
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
|
||||
class CDPSession extends EventEmitter {
|
||||
/**
|
||||
* @param {!Connection} connection
|
||||
* @param {string} targetType
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
constructor(connection, targetType, sessionId) {
|
||||
super();
|
||||
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
|
||||
this._callbacks = new Map();
|
||||
this._connection = connection;
|
||||
this._targetType = targetType;
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {!Object=} params
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
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, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id?: number, method: string, params: Object, error: {message: string, data: any}, result?: *}} object
|
||||
*/
|
||||
_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 {
|
||||
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.CDPSession.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
module.exports = {Connection, CDPSession};
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* 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, debugError, assert} = require('./helper');
|
||||
|
||||
const {EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
|
||||
|
||||
/**
|
||||
* @typedef {Object} CoverageEntry
|
||||
* @property {string} url
|
||||
* @property {string} text
|
||||
* @property {!Array<!{start: number, end: number}>} ranges
|
||||
*/
|
||||
|
||||
class Coverage {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._jsCoverage = new JSCoverage(client);
|
||||
this._cssCoverage = new CSSCoverage(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{resetOnNavigation?: boolean, reportAnonymousScripts?: boolean}} options
|
||||
*/
|
||||
async startJSCoverage(options) {
|
||||
return await this._jsCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!CoverageEntry>>}
|
||||
*/
|
||||
async stopJSCoverage() {
|
||||
return await this._jsCoverage.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{resetOnNavigation?: boolean}=} options
|
||||
*/
|
||||
async startCSSCoverage(options) {
|
||||
return await this._cssCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!CoverageEntry>>}
|
||||
*/
|
||||
async stopCSSCoverage() {
|
||||
return await this._cssCoverage.stop();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Coverage};
|
||||
|
||||
class JSCoverage {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._enabled = false;
|
||||
this._scriptURLs = new Map();
|
||||
this._scriptSources = new Map();
|
||||
this._eventListeners = [];
|
||||
this._resetOnNavigation = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{resetOnNavigation?: boolean, reportAnonymousScripts?: boolean}} options
|
||||
*/
|
||||
async start(options = {}) {
|
||||
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.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Debugger.scriptParsedPayload} event
|
||||
*/
|
||||
async _onScriptParsed(event) {
|
||||
// Ignore puppeteer-injected scripts
|
||||
if (event.url === 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 (e) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!CoverageEntry>>}
|
||||
*/
|
||||
async stop() {
|
||||
assert(this._enabled, 'JSCoverage is not enabled');
|
||||
this._enabled = false;
|
||||
const [profileResponse] = await Promise.all([
|
||||
this._client.send('Profiler.takePreciseCoverage'),
|
||||
this._client.send('Profiler.stopPreciseCoverage'),
|
||||
this._client.send('Profiler.disable'),
|
||||
this._client.send('Debugger.disable'),
|
||||
]);
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
|
||||
const coverage = [];
|
||||
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 {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._enabled = false;
|
||||
this._stylesheetURLs = new Map();
|
||||
this._stylesheetSources = new Map();
|
||||
this._eventListeners = [];
|
||||
this._resetOnNavigation = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{resetOnNavigation?: boolean}=} options
|
||||
*/
|
||||
async start(options = {}) {
|
||||
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.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.CSS.styleSheetAddedPayload} event
|
||||
*/
|
||||
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 (e) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Array<!CoverageEntry>>}
|
||||
*/
|
||||
async stop() {
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<!{startOffset:number, endOffset:number, count:number}>} nestedRanges
|
||||
* @return {!Array<!{start:number, end:number}>}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const {helper, assert} = require('./helper');
|
||||
const {LifecycleWatcher} = require('./LifecycleWatcher');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const readFileAsync = helper.promisify(fs.readFile);
|
||||
|
||||
/**
|
||||
* @unrestricted
|
||||
*/
|
||||
class DOMWorld {
|
||||
/**
|
||||
* @param {!Puppeteer.FrameManager} frameManager
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
* @param {!Puppeteer.TimeoutSettings} timeoutSettings
|
||||
*/
|
||||
constructor(frameManager, frame, timeoutSettings) {
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
|
||||
/** @type {?Promise<!Puppeteer.ElementHandle>} */
|
||||
this._documentPromise = null;
|
||||
/** @type {!Promise<!Puppeteer.ExecutionContext>} */
|
||||
this._contextPromise;
|
||||
this._contextResolveCallback = null;
|
||||
this._setContext(null);
|
||||
|
||||
/** @type {!Set<!WaitTask>} */
|
||||
this._waitTasks = new Set();
|
||||
this._detached = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Puppeteer.Frame}
|
||||
*/
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Puppeteer.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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
_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<!Puppeteer.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<!Puppeteer.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<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
const document = await this._document();
|
||||
const value = await document.$(selector);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Puppeteer.ElementHandle>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const document = await this._document();
|
||||
const value = await document.$x(expression);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
const document = await this._document();
|
||||
return document.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
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<!Puppeteer.ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const document = await this._document();
|
||||
const value = await document.$$(selector);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
*/
|
||||
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(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<!Puppeteer.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');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} type
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} type
|
||||
* @return {!HTMLElement}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{url?: string, path?: string, content?: string}} options
|
||||
* @return {!Promise<!Puppeteer.ElementHandle>}
|
||||
*/
|
||||
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');
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @return {!Promise<!HTMLElement>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.click(options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.focus();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.hover();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values){
|
||||
for (const value of values)
|
||||
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
return this.$eval(selector, (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.tap();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
const handle = await this.$(selector);
|
||||
assert(handle, 'No node found for selector: ' + selector);
|
||||
await handle.type(text, options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
waitForSelector(selector, options) {
|
||||
return this._waitForSelectorOrXPath(selector, false, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
waitForXPath(xpath, options) {
|
||||
return this._waitForSelectorOrXPath(xpath, true, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this.evaluate(() => document.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selectorOrXPath
|
||||
* @param {boolean} isXPath
|
||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
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 waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, 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
|
||||
: document.querySelector(selectorOrXPath);
|
||||
if (!node)
|
||||
return waitForHidden;
|
||||
if (!waitForVisible && !waitForHidden)
|
||||
return node;
|
||||
const element = /** @type {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;
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
function hasVisibleBoundingBox() {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WaitTask {
|
||||
/**
|
||||
* @param {!DOMWorld} domWorld
|
||||
* @param {Function|string} predicateBody
|
||||
* @param {string|number} polling
|
||||
* @param {number} timeout
|
||||
* @param {!Array<*>} args
|
||||
*/
|
||||
constructor(domWorld, predicateBody, title, polling, timeout, ...args) {
|
||||
if (helper.isString(polling))
|
||||
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
||||
else if (helper.isNumber(polling))
|
||||
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
||||
else
|
||||
throw new Error('Unknown polling options: ' + polling);
|
||||
|
||||
this._domWorld = domWorld;
|
||||
this._polling = polling;
|
||||
this._timeout = timeout;
|
||||
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
||||
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 TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
|
||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
||||
}
|
||||
this.rerun();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
terminate(error) {
|
||||
this._terminated = true;
|
||||
this._reject(error);
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
async rerun() {
|
||||
const runCount = ++this._runCount;
|
||||
/** @type {?Puppeteer.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 (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
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(e => 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);
|
||||
this._runningTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} predicateBody
|
||||
* @param {string} polling
|
||||
* @param {number} timeout
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
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<*>}
|
||||
*/
|
||||
function pollMutation() {
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
return Promise.resolve(success);
|
||||
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
const observer = new MutationObserver(mutations => {
|
||||
if (timedOut) {
|
||||
observer.disconnect();
|
||||
fulfill();
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success) {
|
||||
observer.disconnect();
|
||||
fulfill(success);
|
||||
}
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollRaf() {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onRaf();
|
||||
return result;
|
||||
|
||||
function onRaf() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
requestAnimationFrame(onRaf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pollInterval
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
function pollInterval(pollInterval) {
|
||||
let fulfill;
|
||||
const result = new Promise(x => fulfill = x);
|
||||
onTimeout();
|
||||
return result;
|
||||
|
||||
function onTimeout() {
|
||||
if (timedOut) {
|
||||
fulfill();
|
||||
return;
|
||||
}
|
||||
const success = predicate.apply(null, args);
|
||||
if (success)
|
||||
fulfill(success);
|
||||
else
|
||||
setTimeout(onTimeout, pollInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {DOMWorld};
|
|
@ -0,0 +1,848 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
'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': '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
|
||||
}
|
||||
}
|
||||
];
|
||||
for (const device of module.exports)
|
||||
module.exports[device.name] = device;
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 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 {assert} = require('./helper');
|
||||
|
||||
class Dialog {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {string} type
|
||||
* @param {string} message
|
||||
* @param {(string|undefined)} defaultValue
|
||||
*/
|
||||
constructor(client, type, message, defaultValue = '') {
|
||||
this._client = client;
|
||||
this._type = type;
|
||||
this._message = message;
|
||||
this._handled = false;
|
||||
this._defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
message() {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
defaultValue() {
|
||||
return this._defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string=} promptText
|
||||
*/
|
||||
async accept(promptText) {
|
||||
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() {
|
||||
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleJavaScriptDialog', {
|
||||
accept: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Dialog.Type = {
|
||||
Alert: 'alert',
|
||||
BeforeUnload: 'beforeunload',
|
||||
Confirm: 'confirm',
|
||||
Prompt: 'prompt'
|
||||
};
|
||||
|
||||
module.exports = {Dialog};
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
class EmulationManager {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._emulatingMobile = false;
|
||||
this._hasTouch = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Viewport} viewport
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
async emulateViewport(viewport) {
|
||||
const mobile = viewport.isMobile || false;
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
const deviceScaleFactor = viewport.deviceScaleFactor || 1;
|
||||
/** @type {Protocol.Emulation.ScreenOrientation} */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {EmulationManager};
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
class CustomError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutError extends CustomError {}
|
||||
|
||||
module.exports = {
|
||||
TimeoutError,
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const 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'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { Events };
|
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* 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, assert} = require('./helper');
|
||||
const {createJSHandle, JSHandle} = require('./JSHandle');
|
||||
|
||||
const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
|
||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||
|
||||
class ExecutionContext {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
||||
* @param {?Puppeteer.DOMWorld} world
|
||||
*/
|
||||
constructor(client, contextPayload, world) {
|
||||
this._client = client;
|
||||
this._world = world;
|
||||
this._contextId = contextPayload.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.Frame}
|
||||
*/
|
||||
frame() {
|
||||
return this._world ? this._world.frame() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {...*} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async evaluate(pageFunction, ...args) {
|
||||
const handle = await this.evaluateHandle(pageFunction, ...args);
|
||||
const result = await handle.jsonValue().catch(error => {
|
||||
if (error.message.includes('Object reference chain is too long'))
|
||||
return;
|
||||
if (error.message.includes('Object couldn\'t be returned by value'))
|
||||
return;
|
||||
throw error;
|
||||
});
|
||||
await handle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {...*} args
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
|
||||
|
||||
if (helper.isString(pageFunction)) {
|
||||
const contextId = this._contextId;
|
||||
const expression = /** @type {string} */ (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: false,
|
||||
awaitPromise: true,
|
||||
userGesture: true
|
||||
}).catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
|
||||
return 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 (e1) {
|
||||
// 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 (e2) {
|
||||
// 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: false,
|
||||
awaitPromise: true,
|
||||
userGesture: true
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError && err.message === 'Converting circular structure to JSON')
|
||||
err.message += ' Are you passing a nested JSHandle?';
|
||||
throw err;
|
||||
}
|
||||
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
|
||||
return 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) ? 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
* @return {!Protocol.Runtime.evaluateReturnValue}
|
||||
*/
|
||||
function rewriteError(error) {
|
||||
if (error.message.endsWith('Cannot find context with specified id'))
|
||||
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!JSHandle} prototypeHandle
|
||||
* @return {!Promise<!JSHandle>}
|
||||
*/
|
||||
async queryObjects(prototypeHandle) {
|
||||
assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
|
||||
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 createJSHandle(this, response.objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Puppeteer.ElementHandle} elementHandle
|
||||
* @return {Promise<Puppeteer.ElementHandle>}
|
||||
*/
|
||||
async _adoptElementHandle(elementHandle) {
|
||||
assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
|
||||
assert(this._world, 'Cannot adopt handle without DOMWorld');
|
||||
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||
objectId: elementHandle._remoteObject.objectId,
|
||||
});
|
||||
const {object} = await this._client.send('DOM.resolveNode', {
|
||||
backendNodeId: nodeInfo.node.backendNodeId,
|
||||
executionContextId: this._contextId,
|
||||
});
|
||||
return /** @type {Puppeteer.ElementHandle}*/(createJSHandle(this, object));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {ExecutionContext, EVALUATION_SCRIPT_URL};
|
|
@ -0,0 +1,718 @@
|
|||
/**
|
||||
* 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, assert} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
const {ExecutionContext, EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
|
||||
const {LifecycleWatcher} = require('./LifecycleWatcher');
|
||||
const {DOMWorld} = require('./DOMWorld');
|
||||
const {NetworkManager} = require('./NetworkManager');
|
||||
|
||||
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
|
||||
|
||||
class FrameManager extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Puppeteer.Page} page
|
||||
* @param {boolean} ignoreHTTPSErrors
|
||||
* @param {!Puppeteer.TimeoutSettings} timeoutSettings
|
||||
*/
|
||||
constructor(client, page, ignoreHTTPSErrors, timeoutSettings) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._page = page;
|
||||
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors);
|
||||
this._networkManager.setFrameManager(this);
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
/** @type {!Map<string, !Frame>} */
|
||||
this._frames = new Map();
|
||||
/** @type {!Map<number, !ExecutionContext>} */
|
||||
this._contextIdToContext = new Map();
|
||||
/** @type {!Set<string>} */
|
||||
this._isolatedWorlds = new Set();
|
||||
|
||||
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', event => this._onExecutionContextsCleared());
|
||||
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const [,{frameTree}] = await Promise.all([
|
||||
this._client.send('Page.enable'),
|
||||
this._client.send('Page.getFrameTree'),
|
||||
]);
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!NetworkManager}
|
||||
*/
|
||||
networkManager() {
|
||||
return this._networkManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
* @param {string} url
|
||||
* @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
* @return {!Promise<?Puppeteer.Response>}
|
||||
*/
|
||||
async navigateFrame(frame, url, options = {}) {
|
||||
assertNoLegacyNavigationOptions(options);
|
||||
const {
|
||||
referer = this._networkManager.extraHTTPHeaders()['referer'],
|
||||
waitUntil = ['load'],
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const watcher = new 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();
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {string} url
|
||||
* @param {string} referrer
|
||||
* @param {string} frameId
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
* @return {!Promise<?Puppeteer.Response>}
|
||||
*/
|
||||
async waitForFrameNavigation(frame, options = {}) {
|
||||
assertNoLegacyNavigationOptions(options);
|
||||
const {
|
||||
waitUntil = ['load'],
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
const watcher = new 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Page.lifecycleEventPayload} event
|
||||
*/
|
||||
_onLifecycleEvent(event) {
|
||||
const frame = this._frames.get(event.frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._onLifecycleEvent(event.loaderId, event.name);
|
||||
this.emit(Events.FrameManager.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
*/
|
||||
_onFrameStoppedLoading(frameId) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._onLoadingStopped();
|
||||
this.emit(Events.FrameManager.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Page.FrameTree} frameTree
|
||||
*/
|
||||
_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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Puppeteer.Page}
|
||||
*/
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Frame}
|
||||
*/
|
||||
mainFrame() {
|
||||
return this._mainFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!Frame>}
|
||||
*/
|
||||
frames() {
|
||||
return Array.from(this._frames.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!string} frameId
|
||||
* @return {?Frame}
|
||||
*/
|
||||
frame(frameId) {
|
||||
return this._frames.get(frameId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
* @param {?string} parentFrameId
|
||||
*/
|
||||
_onFrameAttached(frameId, parentFrameId) {
|
||||
if (this._frames.has(frameId))
|
||||
return;
|
||||
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.FrameManager.FrameAttached, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Page.Frame} framePayload
|
||||
*/
|
||||
_onFrameNavigated(framePayload) {
|
||||
const isMainFrame = !framePayload.parentId;
|
||||
let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
|
||||
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.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
async _ensureIsolatedWorld(name) {
|
||||
if (this._isolatedWorlds.has(name))
|
||||
return;
|
||||
this._isolatedWorlds.add(name);
|
||||
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
|
||||
worldName: name,
|
||||
}),
|
||||
await Promise.all(this.frames().map(frame => this._client.send('Page.createIsolatedWorld', {
|
||||
frameId: frame._id,
|
||||
grantUniveralAccess: true,
|
||||
worldName: name,
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
* @param {string} url
|
||||
*/
|
||||
_onFrameNavigatedWithinDocument(frameId, url) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (!frame)
|
||||
return;
|
||||
frame._navigatedWithinDocument(url);
|
||||
this.emit(Events.FrameManager.FrameNavigatedWithinDocument, frame);
|
||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} frameId
|
||||
*/
|
||||
_onFrameDetached(frameId) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (frame)
|
||||
this._removeFramesRecursively(frame);
|
||||
}
|
||||
|
||||
_onExecutionContextCreated(contextPayload) {
|
||||
const frameId = contextPayload.auxData ? contextPayload.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);
|
||||
/** @type {!ExecutionContext} */
|
||||
const context = new 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} contextId
|
||||
* @return {!ExecutionContext}
|
||||
*/
|
||||
executionContextById(contextId) {
|
||||
const context = this._contextIdToContext.get(contextId);
|
||||
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Frame} frame
|
||||
*/
|
||||
_removeFramesRecursively(frame) {
|
||||
for (const child of frame.childFrames())
|
||||
this._removeFramesRecursively(child);
|
||||
frame._detach();
|
||||
this._frames.delete(frame._id);
|
||||
this.emit(Events.FrameManager.FrameDetached, frame);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @unrestricted
|
||||
*/
|
||||
class Frame {
|
||||
/**
|
||||
* @param {!FrameManager} frameManager
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {?Frame} parentFrame
|
||||
* @param {string} frameId
|
||||
*/
|
||||
constructor(frameManager, client, parentFrame, frameId) {
|
||||
this._frameManager = frameManager;
|
||||
this._client = client;
|
||||
this._parentFrame = parentFrame;
|
||||
this._url = '';
|
||||
this._id = frameId;
|
||||
this._detached = false;
|
||||
|
||||
this._loaderId = '';
|
||||
/** @type {!Set<string>} */
|
||||
this._lifecycleEvents = new Set();
|
||||
/** @type {!DOMWorld} */
|
||||
this._mainWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
|
||||
/** @type {!DOMWorld} */
|
||||
this._secondaryWorld = new DOMWorld(frameManager, this, frameManager._timeoutSettings);
|
||||
|
||||
/** @type {!Set<!Frame>} */
|
||||
this._childFrames = new Set();
|
||||
if (this._parentFrame)
|
||||
this._parentFrame._childFrames.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
* @return {!Promise<?Puppeteer.Response>}
|
||||
*/
|
||||
async goto(url, options) {
|
||||
return await this._frameManager.navigateFrame(this, url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
* @return {!Promise<?Puppeteer.Response>}
|
||||
*/
|
||||
async waitForNavigation(options) {
|
||||
return await this._frameManager.waitForFrameNavigation(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!ExecutionContext>}
|
||||
*/
|
||||
executionContext() {
|
||||
return this._mainWorld.executionContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<!Puppeteer.JSHandle>}
|
||||
*/
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<*>}
|
||||
*/
|
||||
async evaluate(pageFunction, ...args) {
|
||||
return this._mainWorld.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
return this._mainWorld.$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
return this._mainWorld.$x(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
return this._mainWorld.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
return this._mainWorld.$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<String>}
|
||||
*/
|
||||
async content() {
|
||||
return this._secondaryWorld.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||
*/
|
||||
async setContent(html, options = {}) {
|
||||
return this._secondaryWorld.setContent(html, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
name() {
|
||||
return this._name || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Frame}
|
||||
*/
|
||||
parentFrame() {
|
||||
return this._parentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array.<!Frame>}
|
||||
*/
|
||||
childFrames() {
|
||||
return Array.from(this._childFrames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isDetached() {
|
||||
return this._detached;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{url?: string, path?: string, content?: string, type?: string}} options
|
||||
* @return {!Promise<!Puppeteer.ElementHandle>}
|
||||
*/
|
||||
async addScriptTag(options) {
|
||||
return this._mainWorld.addScriptTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{url?: string, path?: string, content?: string}} options
|
||||
* @return {!Promise<!Puppeteer.ElementHandle>}
|
||||
*/
|
||||
async addStyleTag(options) {
|
||||
return this._mainWorld.addStyleTag(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
|
||||
*/
|
||||
async click(selector, options) {
|
||||
return this._secondaryWorld.click(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async focus(selector) {
|
||||
return this._secondaryWorld.focus(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async hover(selector) {
|
||||
return this._secondaryWorld.hover(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!Array<string>} values
|
||||
* @return {!Promise<!Array<string>>}
|
||||
*/
|
||||
select(selector, ...values){
|
||||
return this._secondaryWorld.select(selector, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
async tap(selector) {
|
||||
return this._secondaryWorld.tap(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(selector, text, options) {
|
||||
return this._mainWorld.type(selector, text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
||||
* @param {!Object=} options
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<?Puppeteer.JSHandle>}
|
||||
*/
|
||||
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
||||
const xPathPattern = '//';
|
||||
|
||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
||||
if (string.startsWith(xPathPattern))
|
||||
return this.waitForXPath(string, options);
|
||||
return this.waitForSelector(string, options);
|
||||
}
|
||||
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
||||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
||||
if (typeof selectorOrFunctionOrTimeout === 'function')
|
||||
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
||||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||
* @return {!Promise<?Puppeteer.ElementHandle>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function|string} pageFunction
|
||||
* @param {!{polling?: string|number, timeout?: number}=} options
|
||||
* @return {!Promise<!Puppeteer.JSHandle>}
|
||||
*/
|
||||
waitForFunction(pageFunction, options = {}, ...args) {
|
||||
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async title() {
|
||||
return this._secondaryWorld.title();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Page.Frame} framePayload
|
||||
*/
|
||||
_navigated(framePayload) {
|
||||
this._name = framePayload.name;
|
||||
// TODO(lushnikov): remove this once requestInterception has loaderId exposed.
|
||||
this._navigationURL = framePayload.url;
|
||||
this._url = framePayload.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
*/
|
||||
_navigatedWithinDocument(url) {
|
||||
this._url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} loaderId
|
||||
* @param {string} name
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoLegacyNavigationOptions(options) {
|
||||
assert(options['networkIdleTimeout'] === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');
|
||||
assert(options['networkIdleInflight'] === undefined, 'ERROR: networkIdleInflight option is no longer supported.');
|
||||
assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead');
|
||||
}
|
||||
|
||||
module.exports = {FrameManager, Frame};
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* 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 {assert} = require('./helper');
|
||||
const keyDefinitions = require('./USKeyboardLayout');
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDescription
|
||||
* @property {number} keyCode
|
||||
* @property {string} key
|
||||
* @property {string} text
|
||||
* @property {string} code
|
||||
* @property {number} location
|
||||
*/
|
||||
|
||||
class Keyboard {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._modifiers = 0;
|
||||
this._pressedKeys = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {{text?: string}=} options
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {number}
|
||||
*/
|
||||
_modifierBit(key) {
|
||||
if (key === 'Alt')
|
||||
return 1;
|
||||
if (key === 'Control')
|
||||
return 2;
|
||||
if (key === 'Meta')
|
||||
return 4;
|
||||
if (key === 'Shift')
|
||||
return 8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} keyString
|
||||
* @return {KeyDescription}
|
||||
*/
|
||||
_keyDescriptionForString(keyString) {
|
||||
const shift = this._modifiers & 8;
|
||||
const description = {
|
||||
key: '',
|
||||
keyCode: 0,
|
||||
code: '',
|
||||
text: '',
|
||||
location: 0
|
||||
};
|
||||
|
||||
const definition = keyDefinitions[keyString];
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} char
|
||||
*/
|
||||
async sendCharacter(char) {
|
||||
await this._client.send('Input.insertText', {text: char});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(text, options) {
|
||||
let delay = 0;
|
||||
if (options && options.delay)
|
||||
delay = options.delay;
|
||||
for (const char of text) {
|
||||
if (keyDefinitions[char])
|
||||
await this.press(char, {delay});
|
||||
else
|
||||
await this.sendCharacter(char);
|
||||
if (delay)
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number, text?: string}=} options
|
||||
*/
|
||||
async press(key, options = {}) {
|
||||
const {delay = null} = options;
|
||||
await this.down(key, options);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, options.delay));
|
||||
await this.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
class Mouse {
|
||||
/**
|
||||
* @param {Puppeteer.CDPSession} client
|
||||
* @param {!Keyboard} keyboard
|
||||
*/
|
||||
constructor(client, keyboard) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this._x = 0;
|
||||
this._y = 0;
|
||||
/** @type {'none'|'left'|'right'|'middle'} */
|
||||
this._button = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {!{steps?: number}=} options
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
|
||||
*/
|
||||
async click(x, y, options = {}) {
|
||||
const {delay = null} = options;
|
||||
this.move(x, y);
|
||||
this.down(options);
|
||||
if (delay !== null)
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
await this.up(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Touchscreen {
|
||||
/**
|
||||
* @param {Puppeteer.CDPSession} client
|
||||
* @param {Keyboard} keyboard
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Keyboard, Mouse, Touchscreen};
|
|
@ -0,0 +1,525 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {helper, assert, debugError} = require('./helper');
|
||||
const path = require('path');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
class JSHandle {
|
||||
/**
|
||||
* @param {!Puppeteer.ExecutionContext} context
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Protocol.Runtime.RemoteObject} remoteObject
|
||||
*/
|
||||
constructor(context, client, remoteObject) {
|
||||
this._context = context;
|
||||
this._client = client;
|
||||
this._remoteObject = remoteObject;
|
||||
this._disposed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Puppeteer.ExecutionContext}
|
||||
*/
|
||||
executionContext() {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} propertyName
|
||||
* @return {!Promise<?JSHandle>}
|
||||
*/
|
||||
async getProperty(propertyName) {
|
||||
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
|
||||
const result = {__proto__: null};
|
||||
result[propertyName] = object[propertyName];
|
||||
return result;
|
||||
}, this, propertyName);
|
||||
const properties = await objectHandle.getProperties();
|
||||
const result = properties.get(propertyName) || null;
|
||||
await objectHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Map<string, !JSHandle>>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Object>}
|
||||
*/
|
||||
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.valueFromRemoteObject(response.result);
|
||||
}
|
||||
return helper.valueFromRemoteObject(this._remoteObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (this._disposed)
|
||||
return;
|
||||
this._disposed = true;
|
||||
await helper.releaseObject(this._client, this._remoteObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {string}
|
||||
*/
|
||||
toString() {
|
||||
if (this._remoteObject.objectId) {
|
||||
const type = this._remoteObject.subtype || this._remoteObject.type;
|
||||
return 'JSHandle@' + type;
|
||||
}
|
||||
return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
|
||||
}
|
||||
}
|
||||
|
||||
class ElementHandle extends JSHandle {
|
||||
/**
|
||||
* @param {!Puppeteer.ExecutionContext} context
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Protocol.Runtime.RemoteObject} remoteObject
|
||||
* @param {!Puppeteer.Page} page
|
||||
* @param {!Puppeteer.FrameManager} frameManager
|
||||
*/
|
||||
constructor(context, client, remoteObject, page, frameManager) {
|
||||
super(context, client, remoteObject);
|
||||
this._client = client;
|
||||
this._remoteObject = remoteObject;
|
||||
this._page = page;
|
||||
this._frameManager = frameManager;
|
||||
this._disposed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @return {?ElementHandle}
|
||||
*/
|
||||
asElement() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Puppeteer.Frame>}
|
||||
*/
|
||||
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.executionContext().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', 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', behavior: 'instant'});
|
||||
return false;
|
||||
}, this, this._page._javascriptEnabled);
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!{x: number, y: number}>}
|
||||
*/
|
||||
async _clickablePoint() {
|
||||
const [result, layoutMetrics] = await Promise.all([
|
||||
this._client.send('DOM.getContentQuads', {
|
||||
objectId: this._remoteObject.objectId
|
||||
}).catch(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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
|
||||
*/
|
||||
_getBoxModel() {
|
||||
return this._client.send('DOM.getBoxModel', {
|
||||
objectId: this._remoteObject.objectId
|
||||
}).catch(error => debugError(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<number>} quad
|
||||
* @return {!Array<{x: number, y: number}>}
|
||||
*/
|
||||
_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]}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<{x: number, y: number}>} quad
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @return {!Array<{x: number, y: number}>}
|
||||
*/
|
||||
_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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
|
||||
*/
|
||||
async click(options) {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} filePaths
|
||||
*/
|
||||
async uploadFile(...filePaths) {
|
||||
const files = filePaths.map(filePath => path.resolve(filePath));
|
||||
const objectId = this._remoteObject.objectId;
|
||||
await this._client.send('DOM.setFileInputFiles', { objectId, files });
|
||||
}
|
||||
|
||||
async tap() {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
const {x, y} = await this._clickablePoint();
|
||||
await this._page.touchscreen.tap(x, y);
|
||||
}
|
||||
|
||||
async focus() {
|
||||
await this.executionContext().evaluate(element => element.focus(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{delay: (number|undefined)}=} options
|
||||
*/
|
||||
async type(text, options) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {!{delay?: number, text?: string}=} options
|
||||
*/
|
||||
async press(key, options) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?{x: number, y: number, width: number, height: number}>}
|
||||
*/
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {!Object=} options
|
||||
* @returns {!Promise<string|!Buffer>}
|
||||
*/
|
||||
async screenshot(options = {}) {
|
||||
let needsViewportReset = false;
|
||||
|
||||
let boundingBox = await this.boundingBox();
|
||||
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();
|
||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
assert(boundingBox.width !== 0, 'Node has 0 width.');
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<?ElementHandle>}
|
||||
*/
|
||||
async $(selector) {
|
||||
const handle = await this.executionContext().evaluateHandle(
|
||||
(element, selector) => element.querySelector(selector),
|
||||
this, selector
|
||||
);
|
||||
const element = handle.asElement();
|
||||
if (element)
|
||||
return element;
|
||||
await handle.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $$(selector) {
|
||||
const arrayHandle = await this.executionContext().evaluateHandle(
|
||||
(element, selector) => element.querySelectorAll(selector),
|
||||
this, selector
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
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 this.executionContext().evaluate(pageFunction, elementHandle, ...args);
|
||||
await elementHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {Function|String} pageFunction
|
||||
* @param {!Array<*>} args
|
||||
* @return {!Promise<(!Object|undefined)>}
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const arrayHandle = await this.executionContext().evaluateHandle(
|
||||
(element, selector) => Array.from(element.querySelectorAll(selector)),
|
||||
this, selector
|
||||
);
|
||||
|
||||
const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args);
|
||||
await arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @return {!Promise<!Array<!ElementHandle>>}
|
||||
*/
|
||||
async $x(expression) {
|
||||
const arrayHandle = await this.executionContext().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;
|
||||
},
|
||||
this, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {!Promise<boolean>}
|
||||
*/
|
||||
isIntersectingViewport() {
|
||||
return this.executionContext().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;
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BoxModel
|
||||
* @property {!Array<!{x: number, y: number}>} content
|
||||
* @property {!Array<!{x: number, y: number}>} padding
|
||||
* @property {!Array<!{x: number, y: number}>} border
|
||||
* @property {!Array<!{x: number, y: number}>} margin
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
*/
|
||||
|
||||
module.exports = {createJSHandle, JSHandle, ElementHandle};
|
|
@ -0,0 +1,443 @@
|
|||
/**
|
||||
* 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 removeFolder = require('rimraf');
|
||||
const childProcess = require('child_process');
|
||||
const BrowserFetcher = require('./BrowserFetcher');
|
||||
const {Connection} = require('./Connection');
|
||||
const {Browser} = require('./Browser');
|
||||
const readline = require('readline');
|
||||
const fs = require('fs');
|
||||
const {helper, assert, debugError} = require('./helper');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
const WebSocketTransport = require('./WebSocketTransport');
|
||||
const PipeTransport = require('./PipeTransport');
|
||||
|
||||
const mkdtempAsync = helper.promisify(fs.mkdtemp);
|
||||
const removeFolderAsync = helper.promisify(removeFolder);
|
||||
|
||||
const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-');
|
||||
|
||||
const DEFAULT_ARGS = [
|
||||
'--disable-background-networking',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-breakpad',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
// TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548
|
||||
// BlinkGenPropertyTrees disabled due to crbug.com/937609
|
||||
'--disable-features=site-per-process,TranslateUI,BlinkGenPropertyTrees',
|
||||
'--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',
|
||||
];
|
||||
|
||||
class Launcher {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {string} preferredRevision
|
||||
* @param {boolean} isPuppeteerCore
|
||||
*/
|
||||
constructor(projectRoot, preferredRevision, isPuppeteerCore) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._preferredRevision = preferredRevision;
|
||||
this._isPuppeteerCore = isPuppeteerCore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
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 chromeArguments = [];
|
||||
if (!ignoreDefaultArgs)
|
||||
chromeArguments.push(...this.defaultArgs(options));
|
||||
else if (Array.isArray(ignoreDefaultArgs))
|
||||
chromeArguments.push(...this.defaultArgs(options).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
|
||||
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(CHROME_PROFILE_PATH);
|
||||
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
|
||||
}
|
||||
if (!chromeArguments.some(arg => arg.startsWith('--profile')) &&
|
||||
env.PROFILE) {
|
||||
chromeArguments.push("--profile");
|
||||
chromeArguments.push(env.PROFILE);
|
||||
}
|
||||
|
||||
let chromeExecutable = executablePath;
|
||||
if (!executablePath) {
|
||||
const {missingText, executablePath} = this._resolveExecutablePath();
|
||||
if (missingText)
|
||||
throw new Error(missingText);
|
||||
chromeExecutable = executablePath;
|
||||
}
|
||||
|
||||
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
|
||||
/** @type {!Array<"ignore"|"pipe">} */
|
||||
const stdio = usePipe ? ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
|
||||
const chromeProcess = childProcess.spawn(
|
||||
chromeExecutable,
|
||||
chromeArguments,
|
||||
{
|
||||
// On non-windows platforms, `detached: false` 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) {
|
||||
chromeProcess.stderr.pipe(process.stderr);
|
||||
chromeProcess.stdout.pipe(process.stdout);
|
||||
}
|
||||
|
||||
let chromeClosed = false;
|
||||
const waitForChromeToClose = new Promise((fulfill, reject) => {
|
||||
chromeProcess.once('exit', () => {
|
||||
chromeClosed = true;
|
||||
// Cleanup as processes exit.
|
||||
if (temporaryUserDataDir) {
|
||||
removeFolderAsync(temporaryUserDataDir)
|
||||
.then(() => fulfill())
|
||||
.catch(err => console.error(err));
|
||||
} else {
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const listeners = [ helper.addEventListener(process, 'exit', killChrome) ];
|
||||
if (handleSIGINT)
|
||||
listeners.push(helper.addEventListener(process, 'SIGINT', () => { killChrome(); process.exit(130); }));
|
||||
if (handleSIGTERM)
|
||||
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseChrome));
|
||||
if (handleSIGHUP)
|
||||
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseChrome));
|
||||
/** @type {?Connection} */
|
||||
let connection = null;
|
||||
try {
|
||||
if (!usePipe) {
|
||||
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout, this._preferredRevision);
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
connection = new Connection(browserWSEndpoint, transport, slowMo);
|
||||
} else {
|
||||
const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]));
|
||||
connection = new Connection('', transport, slowMo);
|
||||
}
|
||||
const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome);
|
||||
await browser.waitForTarget(t => t.type() === 'page');
|
||||
return browser;
|
||||
} catch (e) {
|
||||
killChrome();
|
||||
throw e;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise}
|
||||
*/
|
||||
function gracefullyCloseChrome() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (temporaryUserDataDir) {
|
||||
killChrome();
|
||||
} else if (connection) {
|
||||
// Attempt to close chrome gracefully
|
||||
connection.send('Browser.close').catch(error => {
|
||||
debugError(error);
|
||||
killChrome();
|
||||
});
|
||||
}
|
||||
return waitForChromeToClose;
|
||||
}
|
||||
|
||||
// This method has to be sync to be used as 'exit' event handler.
|
||||
function killChrome() {
|
||||
helper.removeEventListeners(listeners);
|
||||
if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
|
||||
// Force kill chrome.
|
||||
try {
|
||||
if (process.platform === 'win32')
|
||||
childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
|
||||
else
|
||||
process.kill(-chromeProcess.pid, 'SIGKILL');
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
}
|
||||
// Attempt to remove temporary profile directory to avoid littering.
|
||||
try {
|
||||
removeFolder.sync(temporaryUserDataDir);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Launcher.ChromeArgOptions=} options
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
defaultArgs(options = {}) {
|
||||
const {
|
||||
devtools = false,
|
||||
headless = !devtools,
|
||||
args = [],
|
||||
userDataDir = null
|
||||
} = options;
|
||||
const chromeArguments = [...DEFAULT_ARGS];
|
||||
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 (os.platform() === 'win32')
|
||||
chromeArguments.push('--disable-gpu');
|
||||
}
|
||||
if (args.every(arg => arg.startsWith('-')))
|
||||
chromeArguments.push('about:blank');
|
||||
chromeArguments.push(...args);
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
executablePath() {
|
||||
return this._resolveExecutablePath().executablePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
|
||||
* @return {!Promise<!Browser>}
|
||||
*/
|
||||
async connect(options) {
|
||||
const {
|
||||
browserWSEndpoint,
|
||||
browserURL,
|
||||
ignoreHTTPSErrors = false,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
transport,
|
||||
slowMo = 0,
|
||||
} = options;
|
||||
|
||||
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('', transport, slowMo);
|
||||
} else if (browserWSEndpoint) {
|
||||
const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
|
||||
} else if (browserURL) {
|
||||
const connectionURL = await getWSEndpoint(browserURL);
|
||||
const connectionTransport = await WebSocketTransport.create(connectionURL);
|
||||
connection = new Connection(connectionURL, connectionTransport, slowMo);
|
||||
}
|
||||
|
||||
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
|
||||
return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{executablePath: string, missingText: ?string}}
|
||||
*/
|
||||
_resolveExecutablePath() {
|
||||
const browserFetcher = new BrowserFetcher(this._projectRoot);
|
||||
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
|
||||
if (!this._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 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(this._preferredRevision);
|
||||
const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null;
|
||||
return {executablePath: revisionInfo.executablePath, missingText};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.ChildProcess} chromeProcess
|
||||
* @param {number} timeout
|
||||
* @param {string} preferredRevision
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
function waitForWSEndpoint(chromeProcess, timeout, preferredRevision) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rl = readline.createInterface({ input: chromeProcess.stdout });
|
||||
let stderr = '';
|
||||
const listeners = [
|
||||
helper.addEventListener(rl, 'line', onLine),
|
||||
helper.addEventListener(rl, 'close', () => onClose()),
|
||||
helper.addEventListener(chromeProcess, 'exit', () => onClose()),
|
||||
helper.addEventListener(chromeProcess, 'error', error => onClose(error))
|
||||
];
|
||||
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
|
||||
|
||||
/**
|
||||
* @param {!Error=} error
|
||||
*/
|
||||
function onClose(error) {
|
||||
cleanup();
|
||||
reject(new Error([
|
||||
'Failed to launch chrome!' + (error ? ' ' + error.message : ''),
|
||||
stderr,
|
||||
'',
|
||||
'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md',
|
||||
'',
|
||||
].join('\n')));
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
cleanup();
|
||||
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${preferredRevision}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
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.removeEventListeners(listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} browserURL
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
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(e => {
|
||||
e.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + e.message;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Launcher.ChromeArgOptions
|
||||
* @property {boolean=} headless
|
||||
* @property {Array<string>=} args
|
||||
* @property {string=} userDataDir
|
||||
* @property {boolean=} devtools
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Launcher.LaunchOptions
|
||||
* @property {string=} executablePath
|
||||
* @property {boolean|Array<string>=} ignoreDefaultArgs
|
||||
* @property {boolean=} handleSIGINT
|
||||
* @property {boolean=} handleSIGTERM
|
||||
* @property {boolean=} handleSIGHUP
|
||||
* @property {number=} timeout
|
||||
* @property {boolean=} dumpio
|
||||
* @property {!Object<string, string | undefined>=} env
|
||||
* @property {boolean=} pipe
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Launcher.BrowserOptions
|
||||
* @property {boolean=} ignoreHTTPSErrors
|
||||
* @property {(?Puppeteer.Viewport)=} defaultViewport
|
||||
* @property {number=} slowMo
|
||||
*/
|
||||
|
||||
|
||||
module.exports = Launcher;
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {helper, assert} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
const {TimeoutError} = require('./Errors');
|
||||
|
||||
class LifecycleWatcher {
|
||||
/**
|
||||
* @param {!Puppeteer.FrameManager} frameManager
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
* @param {string|!Array<string>} waitUntil
|
||||
* @param {number} timeout
|
||||
*/
|
||||
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[value];
|
||||
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
|
||||
return protocolEvent;
|
||||
});
|
||||
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._initialLoaderId = frame._loaderId;
|
||||
this._timeout = timeout;
|
||||
/** @type {?Puppeteer.Request} */
|
||||
this._navigationRequest = null;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(frameManager._client, Events.CDPSession.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
|
||||
helper.addEventListener(this._frameManager, Events.FrameManager.FrameDetached, this._onFrameDetached.bind(this)),
|
||||
helper.addEventListener(this._frameManager.networkManager(), 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Request} request
|
||||
*/
|
||||
_onRequest(request) {
|
||||
if (request.frame() !== this._frame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
*/
|
||||
_onFrameDetached(frame) {
|
||||
if (this._frame === frame) {
|
||||
this._terminationCallback.call(null, new Error('Navigating frame was detached'));
|
||||
return;
|
||||
}
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.Response}
|
||||
*/
|
||||
navigationResponse() {
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Error} error
|
||||
*/
|
||||
_terminate(error) {
|
||||
this._terminationCallback.call(null, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
sameDocumentNavigationPromise() {
|
||||
return this._sameDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
newDocumentNavigationPromise() {
|
||||
return this._newDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise}
|
||||
*/
|
||||
lifecyclePromise() {
|
||||
return this._lifecyclePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
timeoutOrTerminationPromise() {
|
||||
return Promise.race([this._timeoutPromise, this._terminationPromise]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Error>}
|
||||
*/
|
||||
_createTimeoutPromise() {
|
||||
if (!this._timeout)
|
||||
return new Promise(() => {});
|
||||
const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded';
|
||||
return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
|
||||
.then(() => new TimeoutError(errorMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.Frame} frame
|
||||
*/
|
||||
_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 {!Puppeteer.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.removeEventListeners(this._eventListeners);
|
||||
clearTimeout(this._maximumTimer);
|
||||
}
|
||||
}
|
||||
|
||||
const puppeteerToProtocolLifecycle = {
|
||||
'load': 'load',
|
||||
'domcontentloaded': 'DOMContentLoaded',
|
||||
'networkidle0': 'networkIdle',
|
||||
'networkidle2': 'networkAlmostIdle',
|
||||
};
|
||||
|
||||
module.exports = {LifecycleWatcher};
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @template V
|
||||
*/
|
||||
class Multimap {
|
||||
constructor() {
|
||||
this._map = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @param {V} value
|
||||
*/
|
||||
set(key, value) {
|
||||
let set = this._map.get(key);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this._map.set(key, set);
|
||||
}
|
||||
set.add(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @return {!Set<V>}
|
||||
*/
|
||||
get(key) {
|
||||
let result = this._map.get(key);
|
||||
if (!result)
|
||||
result = new Set();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(key) {
|
||||
return this._map.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @param {V} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasValue(key, value) {
|
||||
const set = this._map.get(key);
|
||||
if (!set)
|
||||
return false;
|
||||
return set.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
get size() {
|
||||
return this._map.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @param {V} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
delete(key, value) {
|
||||
const values = this.get(key);
|
||||
const result = values.delete(value);
|
||||
if (!values.size)
|
||||
this._map.delete(key);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
*/
|
||||
deleteAll(key) {
|
||||
this._map.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} key
|
||||
* @return {V}
|
||||
*/
|
||||
firstValue(key) {
|
||||
const set = this._map.get(key);
|
||||
if (!set)
|
||||
return null;
|
||||
return set.values().next().value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
firstKey() {
|
||||
return this._map.keys().next().value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<V>}
|
||||
*/
|
||||
valuesArray() {
|
||||
const result = [];
|
||||
for (const key of this._map.keys())
|
||||
result.push(...Array.from(this._map.get(key).values()));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<T>}
|
||||
*/
|
||||
keysArray() {
|
||||
return Array.from(this._map.keys());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Multimap;
|
|
@ -0,0 +1,730 @@
|
|||
/**
|
||||
* 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, assert, debugError} = require('./helper');
|
||||
const {Events} = require('./Events');
|
||||
|
||||
class NetworkManager extends EventEmitter {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client, ignoreHTTPSErrors) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._frameManager = null;
|
||||
/** @type {!Map<string, !Request>} */
|
||||
this._requestIdToRequest = new Map();
|
||||
/** @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>} */
|
||||
this._requestIdToRequestWillBeSentEvent = new Map();
|
||||
/** @type {!Object<string, string>} */
|
||||
this._extraHTTPHeaders = {};
|
||||
|
||||
this._offline = false;
|
||||
|
||||
/** @type {?{username: string, password: string}} */
|
||||
this._credentials = null;
|
||||
/** @type {!Set<string>} */
|
||||
this._attemptedAuthentications = new Set();
|
||||
this._userRequestInterceptionEnabled = false;
|
||||
this._protocolRequestInterceptionEnabled = false;
|
||||
this._userCacheDisabled = false;
|
||||
/** @type {!Map<string, string>} */
|
||||
this._requestIdToInterceptionId = new Map();
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Puppeteer.FrameManager} frameManager
|
||||
*/
|
||||
setFrameManager(frameManager) {
|
||||
this._frameManager = frameManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?{username: string, password: string}} credentials
|
||||
*/
|
||||
async authenticate(credentials) {
|
||||
this._credentials = credentials;
|
||||
await this._updateProtocolRequestInterception();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Object<string, string>} extraHTTPHeaders
|
||||
*/
|
||||
async setExtraHTTPHeaders(extraHTTPHeaders) {
|
||||
this._extraHTTPHeaders = {};
|
||||
for (const key of Object.keys(extraHTTPHeaders)) {
|
||||
const value = extraHTTPHeaders[key];
|
||||
assert(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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Object<string, string>}
|
||||
*/
|
||||
extraHTTPHeaders() {
|
||||
return Object.assign({}, this._extraHTTPHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} userAgent
|
||||
*/
|
||||
async setUserAgent(userAgent) {
|
||||
await this._client.send('Network.setUserAgentOverride', { userAgent });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} enabled
|
||||
*/
|
||||
async setCacheEnabled(enabled) {
|
||||
this._userCacheDisabled = !enabled;
|
||||
await this._updateProtocolCacheDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.requestWillBeSentPayload} event
|
||||
*/
|
||||
_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) {
|
||||
/** @type {"Default"|"CancelAuth"|"ProvideCredentials"} */
|
||||
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(debugError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Fetch.requestPausedPayload} event
|
||||
*/
|
||||
_onRequestPaused(event) {
|
||||
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
|
||||
this._client.send('Fetch.continueRequest', {
|
||||
requestId: event.requestId
|
||||
}).catch(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.requestWillBeSentPayload} event
|
||||
* @param {?string} 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 ? this._frameManager.frame(event.frameId) : null;
|
||||
const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
|
||||
this._requestIdToRequest.set(event.requestId, request);
|
||||
this.emit(Events.NetworkManager.Request, request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.requestServedFromCachePayload} event
|
||||
*/
|
||||
_onRequestServedFromCache(event) {
|
||||
const request = this._requestIdToRequest.get(event.requestId);
|
||||
if (request)
|
||||
request._fromMemoryCache = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Request} request
|
||||
* @param {!Protocol.Network.Response} responsePayload
|
||||
*/
|
||||
_handleRequestRedirect(request, responsePayload) {
|
||||
const response = new Response(this._client, request, responsePayload);
|
||||
request._response = response;
|
||||
request._redirectChain.push(request);
|
||||
response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this.emit(Events.NetworkManager.Response, response);
|
||||
this.emit(Events.NetworkManager.RequestFinished, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.responseReceivedPayload} event
|
||||
*/
|
||||
_onResponseReceived(event) {
|
||||
const request = this._requestIdToRequest.get(event.requestId);
|
||||
// FileUpload sends a response without a matching request.
|
||||
if (!request)
|
||||
return;
|
||||
const response = new Response(this._client, request, event.response);
|
||||
request._response = response;
|
||||
this.emit(Events.NetworkManager.Response, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.loadingFinishedPayload} event
|
||||
*/
|
||||
_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()._bodyLoadedPromiseFulfill.call(null);
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this.emit(Events.NetworkManager.RequestFinished, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Network.loadingFailedPayload} event
|
||||
*/
|
||||
_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._bodyLoadedPromiseFulfill.call(null);
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this.emit(Events.NetworkManager.RequestFailed, request);
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {?Puppeteer.Frame} frame
|
||||
* @param {string} interceptionId
|
||||
* @param {boolean} allowInterception
|
||||
* @param {!Protocol.Network.requestWillBeSentPayload} event
|
||||
* @param {!Array<!Request>} redirectChain
|
||||
*/
|
||||
constructor(client, frame, interceptionId, allowInterception, event, redirectChain) {
|
||||
this._client = client;
|
||||
this._requestId = event.requestId;
|
||||
this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
|
||||
this._interceptionId = interceptionId;
|
||||
this._allowInterception = allowInterception;
|
||||
this._interceptionHandled = false;
|
||||
this._response = null;
|
||||
this._failureText = null;
|
||||
|
||||
this._url = event.request.url;
|
||||
this._resourceType = event.type.toLowerCase();
|
||||
this._method = event.request.method;
|
||||
this._postData = event.request.postData;
|
||||
this._headers = {};
|
||||
this._frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
for (const key of Object.keys(event.request.headers))
|
||||
this._headers[key.toLowerCase()] = event.request.headers[key];
|
||||
|
||||
this._fromMemoryCache = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
resourceType() {
|
||||
return this._resourceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
method() {
|
||||
return this._method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
postData() {
|
||||
return this._postData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Object}
|
||||
*/
|
||||
headers() {
|
||||
return this._headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Response}
|
||||
*/
|
||||
response() {
|
||||
return this._response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.Frame}
|
||||
*/
|
||||
frame() {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isNavigationRequest() {
|
||||
return this._isNavigationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!Request>}
|
||||
*/
|
||||
redirectChain() {
|
||||
return this._redirectChain.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?{errorText: string}}
|
||||
*/
|
||||
failure() {
|
||||
if (!this._failureText)
|
||||
return null;
|
||||
return {
|
||||
errorText: this._failureText
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{url?: string, method?:string, postData?: string, headers?: !Object}} overrides
|
||||
*/
|
||||
async continue(overrides = {}) {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this._url.startsWith('data:'))
|
||||
return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
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.
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{status: number, headers: Object, contentType: string, body: (string|Buffer)}} response
|
||||
*/
|
||||
async respond(response) {
|
||||
// Mocking responses for dataURL requests is not currently supported.
|
||||
if (this._url.startsWith('data:'))
|
||||
return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
this._interceptionHandled = true;
|
||||
|
||||
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
|
||||
|
||||
/** @type {!Object<string, string>} */
|
||||
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,
|
||||
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.
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string=} errorCode
|
||||
*/
|
||||
async abort(errorCode = 'failed') {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this._url.startsWith('data:'))
|
||||
return;
|
||||
const errorReason = errorReasons[errorCode];
|
||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
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.
|
||||
debugError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
class Response {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Request} request
|
||||
* @param {!Protocol.Network.Response} responsePayload
|
||||
*/
|
||||
constructor(client, request, responsePayload) {
|
||||
this._client = client;
|
||||
this._request = request;
|
||||
this._contentPromise = null;
|
||||
|
||||
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;
|
||||
this._headers = {};
|
||||
for (const key of Object.keys(responsePayload.headers))
|
||||
this._headers[key.toLowerCase()] = responsePayload.headers[key];
|
||||
this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{ip: string, port: number}}
|
||||
*/
|
||||
remoteAddress() {
|
||||
return this._remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
ok() {
|
||||
return this._status === 0 || (this._status >= 200 && this._status <= 299);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
statusText() {
|
||||
return this._statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Object}
|
||||
*/
|
||||
headers() {
|
||||
return this._headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?SecurityDetails}
|
||||
*/
|
||||
securityDetails() {
|
||||
return this._securityDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Buffer>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<string>}
|
||||
*/
|
||||
async text() {
|
||||
const content = await this.buffer();
|
||||
return content.toString('utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Object>}
|
||||
*/
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Request}
|
||||
*/
|
||||
request() {
|
||||
return this._request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
fromCache() {
|
||||
return this._fromDiskCache || this._request._fromMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
fromServiceWorker() {
|
||||
return this._fromServiceWorker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.Frame}
|
||||
*/
|
||||
frame() {
|
||||
return this._request.frame();
|
||||
}
|
||||
}
|
||||
|
||||
class SecurityDetails {
|
||||
/**
|
||||
* @param {!Protocol.Network.SecurityDetails} securityPayload
|
||||
*/
|
||||
constructor(securityPayload) {
|
||||
this._subjectName = securityPayload['subjectName'];
|
||||
this._issuer = securityPayload['issuer'];
|
||||
this._validFrom = securityPayload['validFrom'];
|
||||
this._validTo = securityPayload['validTo'];
|
||||
this._protocol = securityPayload['protocol'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
subjectName() {
|
||||
return this._subjectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
issuer() {
|
||||
return this._issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validFrom() {
|
||||
return this._validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
validTo() {
|
||||
return this._validTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object<string, string>} headers
|
||||
* @return {!Array<{name: string, value: string}>}
|
||||
*/
|
||||
function headersArray(headers) {
|
||||
const result = [];
|
||||
for (const name in headers)
|
||||
result.push({name, value: headers[name] + ''});
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {Request, Response, NetworkManager, SecurityDetails};
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* 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} = require('./helper');
|
||||
|
||||
/**
|
||||
* @implements {!Puppeteer.ConnectionTransport}
|
||||
*/
|
||||
class PipeTransport {
|
||||
/**
|
||||
* @param {!NodeJS.WritableStream} pipeWrite
|
||||
* @param {!NodeJS.ReadableStream} pipeRead
|
||||
*/
|
||||
constructor(pipeWrite, pipeRead) {
|
||||
this._pipeWrite = pipeWrite;
|
||||
this._pendingMessage = '';
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
|
||||
helper.addEventListener(pipeRead, 'close', () => {
|
||||
if (this.onclose)
|
||||
this.onclose.call(null);
|
||||
})
|
||||
];
|
||||
this.onmessage = null;
|
||||
this.onclose = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
send(message) {
|
||||
this._pipeWrite.write(message);
|
||||
this._pipeWrite.write('\0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Buffer} buffer
|
||||
*/
|
||||
_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.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PipeTransport;
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* 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 = require('./Launcher');
|
||||
const BrowserFetcher = require('./BrowserFetcher');
|
||||
const Errors = require('./Errors');
|
||||
const DeviceDescriptors = require('./DeviceDescriptors');
|
||||
|
||||
module.exports = class {
|
||||
/**
|
||||
* @param {string} projectRoot
|
||||
* @param {string} preferredRevision
|
||||
* @param {boolean} isPuppeteerCore
|
||||
*/
|
||||
constructor(projectRoot, preferredRevision, isPuppeteerCore) {
|
||||
this._projectRoot = projectRoot;
|
||||
this._launcher = new Launcher(projectRoot, preferredRevision, isPuppeteerCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
|
||||
* @return {!Promise<!Puppeteer.Browser>}
|
||||
*/
|
||||
launch(options) {
|
||||
return this._launcher.launch(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
|
||||
* @return {!Promise<!Puppeteer.Browser>}
|
||||
*/
|
||||
connect(options) {
|
||||
return this._launcher.connect(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
executablePath() {
|
||||
return this._launcher.executablePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get devices() {
|
||||
return DeviceDescriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get errors() {
|
||||
return Errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Launcher.ChromeArgOptions=} options
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
defaultArgs(options) {
|
||||
return this._launcher.defaultArgs(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!BrowserFetcher.Options=} options
|
||||
* @return {!BrowserFetcher}
|
||||
*/
|
||||
createBrowserFetcher(options) {
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const {Events} = require('./Events');
|
||||
const {Page} = require('./Page');
|
||||
const {Worker} = require('./Worker');
|
||||
const {Connection} = require('./Connection');
|
||||
|
||||
class Target {
|
||||
/**
|
||||
* @param {!Protocol.Target.TargetInfo} targetInfo
|
||||
* @param {!Puppeteer.BrowserContext} browserContext
|
||||
* @param {!function():!Promise<!Puppeteer.CDPSession>} sessionFactory
|
||||
* @param {boolean} ignoreHTTPSErrors
|
||||
* @param {?Puppeteer.Viewport} defaultViewport
|
||||
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
||||
*/
|
||||
constructor(targetInfo, browserContext, sessionFactory, ignoreHTTPSErrors, defaultViewport, screenshotTaskQueue) {
|
||||
this._targetInfo = targetInfo;
|
||||
this._browserContext = browserContext;
|
||||
this._targetId = targetInfo.targetId;
|
||||
this._sessionFactory = sessionFactory;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
/** @type {?Promise<!Puppeteer.Page>} */
|
||||
this._pagePromise = null;
|
||||
/** @type {?Promise<!Worker>} */
|
||||
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.Page.Popup))
|
||||
return true;
|
||||
const popupPage = await this.page();
|
||||
openerPage.emit(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Puppeteer.CDPSession>}
|
||||
*/
|
||||
createCDPSession() {
|
||||
return this._sessionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Page>}
|
||||
*/
|
||||
async page() {
|
||||
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
|
||||
this._pagePromise = this._sessionFactory()
|
||||
.then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotTaskQueue));
|
||||
}
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<?Worker>}
|
||||
*/
|
||||
async worker() {
|
||||
if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker')
|
||||
return null;
|
||||
if (!this._workerPromise) {
|
||||
this._workerPromise = this._sessionFactory().then(async client => {
|
||||
// Top level workers have a fake page wrapping the actual worker.
|
||||
const [targetAttached] = await Promise.all([
|
||||
new Promise(x => client.once('Target.attachedToTarget', x)),
|
||||
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}),
|
||||
]);
|
||||
const session = Connection.fromSession(client).session(targetAttached.sessionId);
|
||||
// TODO(einbinder): Make workers send their console logs.
|
||||
return new Worker(session, this._targetInfo.url, () => {} /* consoleAPICalled */, () => {} /* exceptionThrown */);
|
||||
});
|
||||
}
|
||||
return this._workerPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._targetInfo.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {"page"|"background_page"|"service_worker"|"shared_worker"|"other"|"browser"}
|
||||
*/
|
||||
type() {
|
||||
const type = this._targetInfo.type;
|
||||
if (type === 'page' || type === 'background_page' || type === 'service_worker' || type === 'shared_worker' || type === 'browser')
|
||||
return type;
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Puppeteer.Browser}
|
||||
*/
|
||||
browser() {
|
||||
return this._browserContext.browser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Puppeteer.BrowserContext}
|
||||
*/
|
||||
browserContext() {
|
||||
return this._browserContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?Puppeteer.Target}
|
||||
*/
|
||||
opener() {
|
||||
const { openerId } = this._targetInfo;
|
||||
if (!openerId)
|
||||
return null;
|
||||
return this.browser()._targets.get(openerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Protocol.Target.TargetInfo} targetInfo
|
||||
*/
|
||||
_targetInfoChanged(targetInfo) {
|
||||
this._targetInfo = targetInfo;
|
||||
|
||||
if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) {
|
||||
this._isInitialized = true;
|
||||
this._initializedCallback(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Target};
|
|
@ -0,0 +1,17 @@
|
|||
class TaskQueue {
|
||||
constructor() {
|
||||
this._chain = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} task
|
||||
* @return {!Promise}
|
||||
*/
|
||||
postTask(task) {
|
||||
const result = this._chain.then(task);
|
||||
this._chain = result.catch(() => {});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {TaskQueue};
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
class TimeoutSettings {
|
||||
constructor() {
|
||||
this._defaultTimeout = null;
|
||||
this._defaultNavigationTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultTimeout(timeout) {
|
||||
this._defaultTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} timeout
|
||||
*/
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._defaultNavigationTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {TimeoutSettings};
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 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, assert} = require('./helper');
|
||||
const fs = require('fs');
|
||||
|
||||
const openAsync = helper.promisify(fs.open);
|
||||
const writeAsync = helper.promisify(fs.write);
|
||||
const closeAsync = helper.promisify(fs.close);
|
||||
|
||||
class Tracing {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
this._recording = false;
|
||||
this._path = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!{path?: string, screenshots?: boolean, categories?: !Array<string>}} options
|
||||
*/
|
||||
async start(options = {}) {
|
||||
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(',')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<!Buffer>}
|
||||
*/
|
||||
async stop() {
|
||||
let fulfill;
|
||||
const contentPromise = new Promise(x => fulfill = x);
|
||||
this._client.once('Tracing.tracingComplete', event => {
|
||||
this._readStream(event.stream, this._path).then(fulfill);
|
||||
});
|
||||
await this._client.send('Tracing.end');
|
||||
this._recording = false;
|
||||
return contentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} handle
|
||||
* @param {?string} path
|
||||
*/
|
||||
async _readStream(handle, path) {
|
||||
let eof = false;
|
||||
let file;
|
||||
if (path)
|
||||
file = await openAsync(path, 'w');
|
||||
const bufs = [];
|
||||
while (!eof) {
|
||||
const response = await this._client.send('IO.read', {handle});
|
||||
eof = response.eof;
|
||||
bufs.push(Buffer.from(response.data));
|
||||
if (path)
|
||||
await writeAsync(file, response.data);
|
||||
}
|
||||
if (path)
|
||||
await closeAsync(file);
|
||||
await this._client.send('IO.close', {handle});
|
||||
let resultBuffer = null;
|
||||
try {
|
||||
resultBuffer = Buffer.concat(bufs);
|
||||
} finally {
|
||||
return resultBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tracing;
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} KeyDefinition
|
||||
* @property {number=} keyCode
|
||||
* @property {number=} shiftKeyCode
|
||||
* @property {string=} key
|
||||
* @property {string=} shiftKey
|
||||
* @property {string=} code
|
||||
* @property {string=} text
|
||||
* @property {string=} shiftText
|
||||
* @property {number=} location
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object<string, KeyDefinition>}
|
||||
*/
|
||||
module.exports = {
|
||||
'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'}
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* 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 WebSocket = require('ws');
|
||||
|
||||
/**
|
||||
* @implements {!Puppeteer.ConnectionTransport}
|
||||
*/
|
||||
class WebSocketTransport {
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {!Promise<!WebSocketTransport>}
|
||||
*/
|
||||
static create(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url, [], { perMessageDeflate: false });
|
||||
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
|
||||
ws.addEventListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!WebSocket} ws
|
||||
*/
|
||||
constructor(ws) {
|
||||
this._ws = ws;
|
||||
this._ws.addEventListener('message', event => {
|
||||
if (this.onmessage)
|
||||
this.onmessage.call(null, event.data);
|
||||
});
|
||||
this._ws.addEventListener('close', event => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
send(message) {
|
||||
this._ws.send(message);
|
||||
}
|
||||
|
||||
close() {
|
||||
this._ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketTransport;
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче