Bug 1632710 - [puppeteer] vendor v3.1.0 r=remote-protocol-reviewers,whimboo,jgraham

This requires a custom mocha reporter under puppeteer/
and changes in output parsing.

Differential Revision: https://phabricator.services.mozilla.com/D77625
This commit is contained in:
Maja Frydrychowicz 2020-06-05 18:53:38 +00:00
Родитель 22b8a4c02c
Коммит 91ed2688dd
199 изменённых файлов: 37826 добавлений и 19174 удалений

Просмотреть файл

@ -106,7 +106,10 @@ _OPT\.OBJ/
^remote/test/puppeteer/node_modules/
^remote/test/puppeteer/.local-chromium/
^remote/test/puppeteer/.local-firefox/
^remote/test/puppeteer/experimental
^remote/test/puppeteer/experimental/
^remote/test/puppeteer/lib/
^remote/test/puppeteer/test/output-firefox
^remote/test/puppeteer/test/output-chromium
# git checkout of libstagefright
^media/libstagefright/android$

Просмотреть файл

@ -6,19 +6,9 @@ test suite] on try. These tests are vendored in central under
_remote/test/puppeteer/_ and we have a script to pull in the most
recent changes.
We currently use [a fork of Puppeteer] with a small number of tweaks
and workaround to make them run against Firefox. These changes
will be upstreamed to Puppeteer when we feel more comfortable about
compatibility with Chrome.
To update the vendored copy of Puppeteer under _remote/test/puppeteer/_:
% ./mach remote vendor-puppeteer
This will replace the current checkout with a fresh copy from
the predefined remote, which currently is the `firefox` branch on
https://github.com/andreastt/puppeteer.git. You can override which
remote and commit to use.
[Testing]: ./Testing.html
[Puppeteer test suite]: https://github.com/GoogleChrome/puppeteer/tree/master/test

Просмотреть файл

@ -109,60 +109,29 @@ Puppeteer tests
In addition to our own Firefox-specific tests, we run the upstream
[Puppeteer test suite] against our implementation to [track progress]
towards achieving full [Puppeteer support] in Firefox.
towards achieving full [Puppeteer support] in Firefox. The tests are written
in the behaviour-driven testing framework [Mocha].
These tests are vendored under _remote/test/puppeteer/_ and are
Puppeteer tests are vendored under _remote/test/puppeteer/_ and are
run locally like this:
% ./mach test remote/test/puppeteer/test
% ./mach puppeteer-test
On try they appear under the `remote(pup)` symbol, but because theyre
a Tier-3 class test job theyre not automatically scheduled.
To schedule the tests, look for `source-test-remote-puppeteer` in
`./mach try fuzzy`.
You can also run them against Chrome as:
The tests are written in the behaviour-driven testing framework
[Mocha], which doesnt support selecting tests by file path like
other harnesses. It does however come with a myriad of flags to
narrow down the selection a bit: some of the most useful ones include
[`--grep`], [`--ignore`], and [`--file`].
% ./mach puppeteer-test --product=chrome --subset
Perhaps the most useful trick is to rename the `describe()` or
`it()` functions to `fdescribe()` and `fit()`, respectively, in
order to run a specific subset of tests. This does however force
you to edit the test files manually.
`--subset` disables a check for missing or skipped tests in our log parsing.
This check is typically not relevant when running against Chrome.
A real-world example:
To schedule the tests on try, look for `source-test-remote-puppeteer` in
`./mach try fuzzy`. On try they appear under the `remote(pup)` symbol.
```diff
diff --git a/remote/test/puppeteer/test/frame.spec.js b/remote/test/puppeteer/test/frame.spec.js
index 58e57934a7b8..0531d49d7a12 100644
--- a/remote/test/puppeteer/test/frame.spec.js
+++ b/remote/test/puppeteer/test/frame.spec.js
@@ -48,7 +48,7 @@ module.exports.addTests = function({testRunner, expect}) {
});
});
Test expectation metadata is collected in _remote/puppeteer-expected.json_
via log parsing and a custom Mocha reporter under
_remote/test/puppeteer/json-mocha-reporter.js_
- describe('Frame.evaluateHandle', function() {
+ fdescribe('Frame.evaluateHandle', function() {
it('should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const mainFrame = page.mainFrame();
@@ -58,7 +58,7 @@ module.exports.addTests = function({testRunner, expect}) {
});
describe('Frame.evaluate', function() {
- it('should throw for detached frames', async({page, server}) => {
+ fit('should throw for detached frames', async({page, server}) => {
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.detachFrame(page, 'frame1');
let error = null;
```
[Puppeteer test suite]: https://github.com/GoogleChrome/puppeteer/tree/master/test
[track progress]: https://docs.google.com/spreadsheets/d/1GZ4yO2-NGD6kbsT7aMlUPUpUqTaASpqNxJGKhOQ-_BM/edit#gid=0
[Puppeteer test suite]: https://github.com/puppeteer/puppeteer/blob/master/test/README.md
[track progress]: https://puppeteer.github.io/ispuppeteerfirefoxready/
[Puppeteer support]: https://bugzilla.mozilla.org/show_bug.cgi?id=puppeteer
[Mocha]: https://mochajs.org/
[`--grep`]: https://mochajs.org/#-grep-regexp-g-regexp
[`--ignore`]: https://mochajs.org/#-ignore-filedirectoryglob
[`--file`]: https://mochajs.org/#-file-filedirectoryglob

Просмотреть файл

@ -12,7 +12,6 @@ import argparse
import json
import sys
import os
import re
import tempfile
import shutil
import subprocess
@ -172,10 +171,13 @@ def npm(*args, **kwargs):
return p.returncode
def wait_proc(p, cmd=None, exit_on_fail=True):
def wait_proc(p, cmd=None, exit_on_fail=True, output_timeout=None):
try:
p.run()
p.run(outputTimeout=output_timeout)
p.wait()
if p.timedOut:
# In some cases, we wait longer for a mocha timeout
print("Timed out after {} seconds of no output".format(output_timeout))
finally:
p.kill()
if exit_on_fail and p.returncode > 0:
@ -187,8 +189,6 @@ def wait_proc(p, cmd=None, exit_on_fail=True):
class MochaOutputHandler(object):
def __init__(self, logger, expected):
self.logger = logger
self.test_name_re = re.compile(r"\d+\) \[\s*(\w+)\s*\] (.*) \(.*\)")
self.control_re = re.compile("\x1b\\[[0-9]*m")
self.proc = None
self.test_results = OrderedDict()
self.expected = expected
@ -199,7 +199,9 @@ class MochaOutputHandler(object):
"CRASHED": "CRASH",
"OK": "PASS",
"TERMINATED": "CRASH",
"TIME": "TIMEOUT",
"pass": "PASS",
"fail": "FAIL",
"pending": "SKIP"
}
@property
@ -207,14 +209,39 @@ class MochaOutputHandler(object):
return self.proc and self.proc.pid
def __call__(self, line):
line_text = self.control_re.subn("", line)[0]
m = self.test_name_re.match(line_text)
if m:
status, test_name = m.groups()
status = self.status_map.get(status, status)
event = None
try:
if line.startswith('[') and line.endswith(']'):
event = json.loads(line)
self.process_event(event)
except ValueError:
pass
finally:
self.logger.process_output(self.pid, line, command="npm")
def process_event(self, event):
if isinstance(event, list) and len(event) > 1:
status = self.status_map.get(event[0])
test_start = event[0] == 'test-start'
if not status and not test_start:
return
test_info = event[1]
test_name = test_info.get("fullTitle", "")
test_path = test_info.get("file", "")
test_err = test_info.get("err")
if status == "FAIL" and test_err:
if "timeout" in test_err.lower():
status = "TIMEOUT"
if test_name and test_path:
test_name = "{} ({})".format(test_name, os.path.basename(test_path))
if test_start:
self.logger.test_start(test_name)
return
# mozlog doesn't really allow unexpected skip,
# so if a test is disabled just expect that
# so if a test is disabled just expect that.
# Also, mocha doesn't log test-start for skipped tests
if status == "SKIP":
self.logger.test_start(test_name)
expected = ["SKIP"]
else:
expected = self.expected.get(test_name, ["PASS"])
@ -222,8 +249,6 @@ class MochaOutputHandler(object):
expected_status = expected[0]
self.test_results[test_name] = status
self.logger.test_start(test_name)
self.logger.test_end(test_name,
status=status,
expected=expected_status,
@ -231,7 +256,6 @@ class MochaOutputHandler(object):
if status not in expected:
self.has_unexpected = True
self.logger.process_output(self.pid, line, command="npm")
def new_expected(self):
new_expected = OrderedDict()
@ -315,16 +339,29 @@ class PuppeteerRunner(MozbuildObject):
binary = params.get("binary") or self.get_binary_path()
product = params.get("product", "firefox")
env = {"DUMPIO": "1"}
env = {
# Print browser process ouptut
"DUMPIO": "1",
# Checked by Puppeteer's custom mocha config
"CI": "1",
# Causes some tests to be skipped due to assumptions about install
"PUPPETEER_ALT_INSTALL": "1"
}
extra_options = {}
for k, v in params.get("extra_launcher_options", {}).items():
extra_options[k] = json.loads(v)
# Override upstream defaults: no retries, shorter timeout
mocha_options = [
"--reporter", "./json-mocha-reporter.js",
"--retries", "0",
"--fullTrace",
"--timeout", "15000"
]
if product == "firefox":
env["BINARY"] = binary
command = ["run", "funit", "--", "--verbose"]
elif product == "chrome":
command = ["run", "unit", "--", "--verbose"]
env["PUPPETEER_PRODUCT"] = "firefox"
command = ["run", "unit", "--"] + mocha_options
if params.get("jobs"):
env["PPTR_PARALLEL_TESTS"] = str(params["jobs"])
@ -353,7 +390,9 @@ class PuppeteerRunner(MozbuildObject):
processOutputLine=output_handler, wait=False)
output_handler.proc = proc
wait_proc(proc, "npm", exit_on_fail=False)
# Puppeteer unit tests don't always clean-up child processes in case of
# failure, so use an output_timeout as a fallback
wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False)
output_handler.after_end(params.get("subset", False))
@ -507,10 +546,24 @@ class PuppeteerTest(MachCommandBase):
def install_puppeteer(self, product):
setup()
env = {}
from mozversioncontrol import get_repository_object
repo = get_repository_object(self.topsrcdir)
puppeteer_dir = os.path.join("remote", "test", "puppeteer")
src_dir = os.path.join(puppeteer_dir, "src")
changed_files = False
for f in repo.get_changed_files():
if f.startswith(src_dir):
changed_files = True
break
if product != "chrome":
env["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "1"
env["PUPPETEER_SKIP_DOWNLOAD"] = "1"
lib_dir = os.path.join(self.topsrcdir, puppeteer_dir, "lib")
if changed_files and os.path.isdir(lib_dir):
# clobber lib to force `tsc compile` step
shutil.rmtree(lib_dir)
npm("install",
cwd=os.path.join(self.topsrcdir, "remote", "test", "puppeteer"),
cwd=os.path.join(self.topsrcdir, puppeteer_dir),
env=env)

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,19 +0,0 @@
environment:
matrix:
- nodejs_version: "8.16.0"
FLAKINESS_DASHBOARD_NAME: Appveyor Chromium (Win + node8)
FLAKINESS_DASHBOARD_PASSWORD:
secure: g66jP+j6C+hkXLutBV9fdxB5fRJgcQQzy93SgQzXUmcCl/RjkJwnzyHvX0xfCVnv
build: off
install:
- ps: $env:FLAKINESS_DASHBOARD_BUILD_URL="https://ci.appveyor.com/project/aslushnikov/puppeteer/builds/$env:APPVEYOR_BUILD_ID/job/$env:APPVEYOR_JOB_ID"
- ps: Install-Product node $env:nodejs_version
- npm install
- if "%nodejs_version%" == "8.16.0" (
npm run lint &&
npm run coverage &&
npm run test-doclint &&
npm run test-types
)

Просмотреть файл

@ -2,7 +2,7 @@ FROM node:10
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 \
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 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 && \

Просмотреть файл

@ -2,7 +2,7 @@ FROM node:12
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 \
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 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 && \

Просмотреть файл

@ -1,17 +0,0 @@
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

Просмотреть файл

@ -1,47 +0,0 @@
env:
DISPLAY: :99.0
FLAKINESS_DASHBOARD_PASSWORD: ENCRYPTED[b3e207db5d153b543f219d3c3b9123d8321834b783b9e45ac7d380e026ab3a56398bde51b521ac5859e7e45cb95d0992]
FLAKINESS_DASHBOARD_NAME: Cirrus ${CIRRUS_TASK_NAME}
FLAKINESS_DASHBOARD_BUILD_URL: https://cirrus-ci.com/task/${CIRRUS_TASK_ID}
task:
matrix:
- name: Chromium (node8 + linux)
container:
dockerfile: .ci/node8/Dockerfile.linux
- name: Chromium (node10 + linux)
container:
dockerfile: .ci/node10/Dockerfile.linux
- name: Chromium (node12 + linux)
container:
dockerfile: .ci/node12/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 Juggler (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 fjunit
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

Просмотреть файл

@ -2,8 +2,13 @@ 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/
lib/
src/externs.d.ts
src/protocol.d.ts
/index.d.ts
# We ignore this file because it uses ES imports which we don't yet use
# in the Puppeteer src, so it trips up the ESLint-TypeScript parser.
utils/doclint/generate_types/test/test.ts

Просмотреть файл

@ -1,55 +1,41 @@
module.exports = {
"root": true,
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 9
},
"parser": "@typescript-eslint/parser",
"plugins": [
"mocha",
"@typescript-eslint",
"unicorn"
],
"extends": [
"plugin:prettier/recommended"
],
/**
* 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
*/
// Error if files are not formatted with Prettier correctly.
"prettier/prettier": 2,
// 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,
@ -78,35 +64,32 @@ module.exports = {
"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
}],
// ensure we don't have any it.only or describe.only in prod
"mocha/no-exclusive-tests": "error",
// 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
}]
}
// enforce the variable in a catch block is named error
"unicorn/catch-error-name": "error"
},
"overrides": [
{
"files": ["*.ts"],
"extends": [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
"rules": {
"no-unused-vars": 0,
"@typescript-eslint/no-unused-vars": 2,
"semi": 0,
"@typescript-eslint/semi": 2,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-use-before-define": 0,
// We know it's bad and use it very sparingly but it's needed :(
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/array-type": [2, {
"default": "array-simple"
}]
}
}
]
};

Просмотреть файл

@ -1,44 +0,0 @@
.appveyor.yml
.gitattributes
# no longer generated, but old checkouts might still have it
node6
# exclude all tests
test
utils/node6-transform
# exclude internal type definitions
/lib/externs.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/puppeteer/puppeteer/issues/3878
/index.d.ts

Просмотреть файл

@ -1,49 +1,82 @@
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"
script:
- 'if [ "$NODE8" = "true" ]; then npm run lint; fi'
- 'if [ "$NODE8" = "true" ]; then npm run coverage; fi'
- 'if [ "$FIREFOX" = "true" ]; then cd experimental/puppeteer-firefox && npm i && cd ../..; fi'
- 'if [ "$FIREFOX" = "true" ]; then npm run fjunit; 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'
services: xvfb
jobs:
include:
- node_js: "8.16.0"
- os: "osx"
name: 'Unit tests: macOS/Chromium'
node_js: "10.19.0"
env:
- NODE8=true
- FLAKINESS_DASHBOARD_NAME="Travis Chromium (node8 + linux)"
- FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}"
- node_js: "8.16.0"
- CHROMIUM=true
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- ls .local-chromium .local-firefox
- npm run tsc
- travis_retry npm run unit
- os: "windows"
name: 'Unit tests: Windows/Chromium'
node_js: "10.19.0"
env:
- CHROMIUM=true
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- ls .local-chromium .local-firefox
- npm run tsc
- travis_retry npm run unit
# Runs unit tests on Linux + Chromium
- node_js: "10.19.0"
name: 'Unit tests [with coverage]: Linux/Chromium'
env:
- CHROMIUM=true
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- travis_retry npm run unit-with-coverage
- npm run assert-unit-coverage
- node_js: "12.16.3"
name: 'Unit tests [Node 12]: Linux/Chromium'
env:
- CHROMIUM=true
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- travis_retry npm run unit
- node_js: "14.2.0"
name: 'Unit tests [Node 14]: Linux/Chromium'
env:
- CHROMIUM=true
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- travis_retry npm run unit
# This bot runs all the extra checks that aren't the main Puppeteer unit tests
- node_js: "10.19.0"
name: 'Extra tests: Linux/Chromium'
env:
- CHROMIUM=true
script:
- npm run compare-protocol-d-ts
- npm run test-install
- npm run lint
- npm run test-doclint
- npm run test-types
# Runs unit tests on Linux + Firefox
- node_js: "10.19.0"
name: 'Unit tests: Linux/Firefox'
env:
- FIREFOX=true
- FLAKINESS_DASHBOARD_NAME="Travis Firefox (node8 + linux)"
- FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}"
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
before_install:
- PUPPETEER_PRODUCT=firefox npm install
script:
- travis_retry npm run funit
notifications:
email: false

Просмотреть файл

@ -4,6 +4,7 @@
* [Getting Code](#getting-code)
* [Code reviews](#code-reviews)
* [Code Style](#code-style)
* [TypeScript guidelines](#typescript-guidelines)
* [API guidelines](#api-guidelines)
* [Commit Messages](#commit-messages)
* [Writing Documentation](#writing-documentation)
@ -63,16 +64,27 @@ information on using pull requests.
## Code Style
- Coding style is fully defined in [.eslintrc](https://github.com/puppeteer/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.
- Coding style is fully defined in [`.eslintrc`](https://github.com/puppeteer/puppeteer/blob/master/.eslintrc.js) and we automatically format our code with [Prettier](https://prettier.io).
- It's recommended to set-up Prettier into your editor, or you can run `npm run eslint-fix` to automatically format any files.
- If you're working in a JS file, code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler).
- If you're working in a TS file, you should explicitly type all variables and return types. You'll get ESLint warnings if you don't so if you're not sure use them as guidelines, and feel free to ask us for help!
To run code linter, use:
To run ESLint, use:
```bash
npm run lint
npm run eslint
```
You can check your code (both JS & TS) type-checks by running:
```bash
npm run tsc
```
## TypeScript guidelines
- Try to avoid the use of `any` when possible. Consider `unknown` as a better alternative. You are able to use `any` if needbe, but it will generate an ESLint warning.
## API guidelines
When authoring new API methods, consider the following:
@ -145,11 +157,11 @@ A barrier for introducing new installation dependencies is especially high:
- 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 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/puppeteer/puppeteer/blob/master/test/test.js)
and are written with a [TestRunner](https://github.com/puppeteer/puppeteer/tree/master/utils/testrunner) framework.
Puppeteer tests are located in the test directory ([`test`](https://github.com/puppeteer/puppeteer/blob/master/test/) and are written using Mocha. See [`test/README.md`](https://github.com/puppeteer/puppeteer/blob/master/test/) for more details.
Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected.
- To run all tests:
@ -158,25 +170,11 @@ Despite being named 'unit', these are integration tests, making sure public API
npm run unit
```
- To run tests in parallel, use `-j` flag:
```bash
npm run unit -- -j 4
```
- To run tests in "verbose" mode or to stop testrunner on first failure:
```bash
npm run unit -- --verbose
npm run unit -- --break-on-failure
```
- To run a specific test, substitute the `it` with `fit` (mnemonic rule: '*focus it*'):
- To run a specific test, substitute the `it` with `it.only`:
```js
...
// Using "fit" to run specific test
fit('should work', async function({server, page}) {
it.only('should work', async function({server, page}) {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok).toBe(true);
});
@ -199,31 +197,19 @@ npm run unit -- --break-on-failure
HEADLESS=false npm run unit
```
- To run Firefox tests, firstly ensure you have Firefox installed locally (you only need to do this once, not on every test run) and then you can run the tests:
```bash
PUPPETEER_PRODUCT=firefox node install.js
PUPPETEER_PRODUCT=firefox npm run unit
```
- To run tests with custom browser executable:
```bash
BINARY=<path-to-executable> npm run unit
```
- To run tests in slow-mode:
```bash
HEADLESS=false SLOW_MO=500 npm run unit
```
- To run tests with additional Launcher options:
```bash
EXTRA_LAUNCH_OPTIONS='{"args": ["--user-data-dir=some/path"], "handleSIGINT": true}' 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.
@ -247,7 +233,7 @@ Releasing to npm consists of the following phases:
1. Source Code: mark a release.
1. Bump `package.json` version following the SEMVER rules.
2. Run `npm run doc` to update the docs accordingly.
3. Update the “Releases per Chromium Version” list in [`docs/api.md`](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md) to include the new version.
3. Update the “Releases per Chromium Version” list in [`docs/api.md`](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md) to include the new version. Note: only do this when the Chrome revision is different from the previous release.
4. Send a PR titled `'chore: mark version vXXX.YYY.ZZZ'` ([example](https://github.com/puppeteer/puppeteer/pull/5078)).
5. 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/puppeteer/puppeteer/pull/2446)
@ -259,9 +245,8 @@ Releasing to npm consists of the following phases:
1. On your local machine, pull from [upstream](https://github.com/puppeteer/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 adding unnecessary files to the npm package.
3. Run `npm install` to make sure the latest `lib/protocol.d.ts` is generated.
4. Run [`npx pkgfiles`](https://www.npmjs.com/package/pkgfiles) to make sure you don't publish anything unnecessary.
5. Run `npm publish`. This publishes the `puppeteer` package.
3. Run [`npx pkgfiles`](https://www.npmjs.com/package/pkgfiles) to make sure you don't publish anything unnecessary.
4. Run `npm publish`. This publishes the `puppeteer` package.
3. Publish `puppeteer-core` to npm.
1. Run `./utils/prepare_puppeteer_core.js`. The script changes the name inside `package.json` to `puppeteer-core`.
2. Run `npm publish`. This publishes the `puppeteer-core` package.
@ -272,22 +257,16 @@ Releasing to npm consists of the following phases:
## Updating npm dist tags
For both `puppeteer` and `puppeteer-core` we maintain the following npm tags:
- `chrome-*` tags, e.g. `chrome-75` and so on. These tags match the Puppeteer version that corresponds to the `chrome-*` release.
- `chrome-stable` tag. This tag points to the Puppeteer version that works with the current Chrome stable release.
For both `puppeteer` and `puppeteer-core` we maintain `chrome-*` npm dist tags, e.g. `chrome-75` and so on. These tags match the Puppeteer version that corresponds to the `chrome-*` release.
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*.
Managing tags 101:
```bash
# list tags
# 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
# Add tags
$ npm dist-tag add puppeteer@3.0.0 chrome-81
$ npm dist-tag add puppeteer-core@3.0.0 chrome-81
```

Просмотреть файл

@ -1,12 +1,12 @@
# Puppeteer
<!-- [START badges] -->
[![Linux Build Status](https://img.shields.io/travis/com/puppeteer/puppeteer/master.svg)](https://travis-ci.com/puppeteer/puppeteer) [![Windows Build Status](https://img.shields.io/appveyor/ci/mathiasbynens/puppeteer/master.svg?logo=appveyor)](https://ci.appveyor.com/project/mathiasbynens/puppeteer/branch/master) [![Build Status](https://api.cirrus-ci.com/github/puppeteer/puppeteer.svg)](https://cirrus-ci.com/github/puppeteer/puppeteer) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) [![Issue resolution status](https://isitmaintained.com/badge/resolution/puppeteer/puppeteer.svg)](https://github.com/puppeteer/puppeteer/issues)
[![Build status](https://img.shields.io/travis/com/puppeteer/puppeteer/master.svg)](https://travis-ci.com/puppeteer/puppeteer) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) [![Issue resolution status](https://isitmaintained.com/badge/resolution/puppeteer/puppeteer.svg)](https://github.com/puppeteer/puppeteer/issues)
<!-- [END badges] -->
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right">
###### [API](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md)
###### [API](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md)
> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium.
@ -37,13 +37,13 @@ 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/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#environment-variables).
Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v3.1.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.
a version of Puppeteer that doesn't download any browser by default.
```bash
npm i puppeteer-core
@ -59,11 +59,11 @@ See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/ma
Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node.
Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. All subsequent versions rely on
Node 8.9.0+. All examples below use async/await which is only supported in Node v7.6.0 or greater.
Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on
Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater.
Puppeteer will be familiar to people using other browser testing frameworks. You create an instance
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#).
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#).
**Example** - navigating to https://example.com and saving a screenshot as *example.png*:
@ -88,7 +88,7 @@ Execute script on the command line
node example.js
```
Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#pagesetviewportviewport).
Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#pagesetviewportviewport).
**Example** - create a PDF.
@ -113,7 +113,7 @@ Execute script on the command line
node hn.js
```
See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#pagepdfoptions) for more information about creating pdfs.
See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#pagepdfoptions) for more information about creating pdfs.
**Example** - evaluate script in the context of the page
@ -148,7 +148,7 @@ Execute script on the command line
node get-dimensions.js
```
See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`.
See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`.
<!-- [END getstarted] -->
@ -157,7 +157,7 @@ See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/
**1. Uses Headless mode**
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#puppeteerlaunchoptions) when launching a browser:
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#puppeteerlaunchoptions) when launching a browser:
```js
const browser = await puppeteer.launch({headless: false}); // default is true
@ -173,19 +173,19 @@ pass in the executable's path when creating a `Browser` instance:
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
```
See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#puppeteerlaunchoptions) for more information.
You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v3.1.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**.
Puppeteer creates its own browser user profile which it **cleans up on every run**.
<!-- [END runtimesettings] -->
## Resources
- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md)
- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md)
- [Examples](https://github.com/puppeteer/puppeteer/tree/master/examples/)
- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer)
@ -198,22 +198,28 @@ Puppeteer creates its own Chromium user profile which it **cleans up on every ru
displaying. Instead of launching in headless mode, launch a full version of
the browser using `headless: false`:
const browser = await puppeteer.launch({headless: false});
```js
const browser = await puppeteer.launch({headless: false});
```
2. Slow it down - the `slowMo` option slows down Puppeteer operations by the
specified amount of milliseconds. It's another way to help see what's going on.
const browser = await puppeteer.launch({
headless: false,
slowMo: 250 // slow down by 250ms
});
```js
const browser = await puppeteer.launch({
headless: false,
slowMo: 250 // slow down by 250ms
});
```
3. Capture console output - You can listen for the `console` event.
This is also handy when debugging code in `page.evaluate()`:
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
```js
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.evaluate(() => console.log(`url is ${location.href}`));
await page.evaluate(() => console.log(`url is ${location.href}`));
```
4. Use debugger in application code browser
@ -223,7 +229,9 @@ Puppeteer creates its own Chromium user profile which it **cleans up on every ru
- Use `{devtools: true}` when launching Puppeteer:
`const browser = await puppeteer.launch({devtools: true});`
```js
const browser = await puppeteer.launch({devtools: true});
```
- Change default test timeout:
@ -235,7 +243,9 @@ Puppeteer creates its own Chromium user profile which it **cleans up on every ru
- Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement:
`await page.evaluate(() => {debugger;});`
```js
await page.evaluate(() => {debugger;});
```
The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode.
@ -248,10 +258,12 @@ Puppeteer creates its own Chromium user profile which it **cleans up on every ru
you want to try something out, you have to add it to your test file.
- Add `debugger;` to your test, eg:
```
```js
debugger;
await page.click('a[target=_blank]');
```
- Set `headless` to `false`
- Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests`
- In Chrome open `chrome://inspect/#devices` and click `inspect`
@ -268,7 +280,7 @@ Puppeteer creates its own Chromium user profile which it **cleans up on every ru
# 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/puppeteerLabs/ndb)
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)!)
@ -296,6 +308,15 @@ Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/maste
The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project!
See [Contributing](https://github.com/puppeteer/puppeteer/blob/master/CONTRIBUTING.md).
#### Q: What is the status of cross-browser support?
Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention.
From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox.
We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari.
This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome).
#### Q: What are Puppeteers goals and principles?
The goals of the project are:
@ -345,6 +366,18 @@ npm install puppeteer-core@chrome-71
Look for `chromium_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/master/package.json). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section.
#### Q: Which Firefox version does Puppeteer use?
Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox_revision` in [package.json](https://github.com/puppeteer/puppeteer/blob/master/package.json) is `latest` -- Puppeteer isn't tied to a particular Firefox version.
To fetch Firefox Nightly as part of Puppeteer installation:
```bash
PUPPETEER_PRODUCT=firefox npm i puppeteer
# or "yarn add puppeteer"
```
#### Q: Whats considered a “Navigation”?
From Puppeteers standpoint, **“navigation” is anything that changes a pages URL**.
@ -375,7 +408,7 @@ await page.evaluate(() => {
You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this:
* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.)
* Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v3.1.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?

Просмотреть файл

@ -1,11 +1,14 @@
# Puppeteer API <!-- GEN:version -->Tip-Of-Tree<!-- GEN:stop-->
# Puppeteer API <!-- GEN:version -->v3.1.0<!-- GEN:stop-->
<!-- GEN:empty-if-release --><!-- GEN:stop -->
- Interactive Documentation: https://pptr.dev
- API Translations: [中文|Chinese](https://zhaoqize.github.io/puppeteer-api-zh_CN/#/)
- Troubleshooting: [troubleshooting.md](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md)
- Releases per Chromium Version:
* Chromium 83.0.4103.0 - [Puppeteer v3.1.0](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md)
* Chromium 81.0.4044.0 - [Puppeteer v3.0.0](https://github.com/puppeteer/puppeteer/blob/v3.0.0/docs/api.md)
* Chromium 80.0.3987.0 - [Puppeteer v2.1.0](https://github.com/puppeteer/puppeteer/blob/v2.1.0/docs/api.md)
* Chromium 79.0.3942.0 - [Puppeteer v2.0.0](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md)
* Chromium 78.0.3882.0 - [Puppeteer v1.20.0](https://github.com/puppeteer/puppeteer/blob/v1.20.0/docs/api.md)
* Chromium 77.0.3803.0 - [Puppeteer v1.19.0](https://github.com/puppeteer/puppeteer/blob/v1.19.0/docs/api.md)
@ -35,8 +38,10 @@
- [class: BrowserFetcher](#class-browserfetcher)
* [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision)
* [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback)
* [browserFetcher.host()](#browserfetcherhost)
* [browserFetcher.localRevisions()](#browserfetcherlocalrevisions)
* [browserFetcher.platform()](#browserfetcherplatform)
* [browserFetcher.product()](#browserfetcherproduct)
* [browserFetcher.remove(revision)](#browserfetcherremoverevision)
* [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision)
- [class: Browser](#class-browser)
@ -307,6 +312,7 @@
- [class: SecurityDetails](#class-securitydetails)
* [securityDetails.issuer()](#securitydetailsissuer)
* [securityDetails.protocol()](#securitydetailsprotocol)
* [securityDetails.subjectAlternativeNames()](#securitydetailssubjectalternativenames)
* [securityDetails.subjectName()](#securitydetailssubjectname)
* [securityDetails.validFrom()](#securitydetailsvalidfrom)
* [securityDetails.validTo()](#securitydetailsvalidto)
@ -370,6 +376,7 @@ In most cases, you'll be fine using the `puppeteer` package.
However, you should use `puppeteer-core` if:
- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `puppeteer-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space.
- you're bundling Puppeteer to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary.
- you're building a set of tools where `puppeteer-core` is one of the ingredients and you want to postpone `install.js` script execution until Chromium is about to be used.
When using `puppeteer-core`, remember to change the *include* line:
@ -389,7 +396,7 @@ If Puppeteer doesn't find them in the environment during the installation step,
- `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`.
- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
- `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in `puppeteer.launch`. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how the executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. Setting `product` programmatically in [puppeteer.launch([options])](#puppeteerlaunchoptions) supercedes this environment variable. The product is exposed in [`puppeteer.product`](#puppeteerproduct)
- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This can also be used during installation to fetch the recommended browser binary. Setting `product` programmatically in [puppeteer.launch([options])](#puppeteerlaunchoptions) supersedes this environment variable. The product is exposed in [`puppeteer.product`](#puppeteerproduct)
> **NOTE** PUPPETEER_* env variables are not accounted for in the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package.
@ -453,15 +460,17 @@ const puppeteer = require('puppeteer');
- `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`.
- `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Puppeteer to use.
- `product` <[string]> Possible values are: `chrome`, `firefox`. Defaults to `chrome`.
- returns: <[Promise]<[Browser]>>
This methods attaches Puppeteer to an existing Chromium instance.
This methods attaches Puppeteer to an existing browser instance.
#### puppeteer.createBrowserFetcher([options])
- `options` <[Object]>
- `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`.
- `path` <[string]> A path for the downloads folder. Defaults to `<root>/.local-chromium`, where `<root>` is puppeteer's package root.
- `platform` <[string]> Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform.
- `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`. If the `product` is `firefox`, this defaults to `https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central`.
- `path` <[string]> A path for the downloads folder. Defaults to `<root>/.local-chromium`, where `<root>` is puppeteer's package root. If the `product` is `firefox`, this defaults to `<root>/.local-firefox`.
- `platform` <"linux"|"mac"|"win32"|"win64"> [string] for the current platform. Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform.
- `product` <"chrome"|"firefox"> [string] for the product to run. Possible values are: `chrome`, `firefox`. Defaults to `chrome`.
- returns: <[BrowserFetcher]>
#### puppeteer.defaultArgs([options])
@ -478,7 +487,7 @@ The default flags that Chromium will be launched with.
- returns: <[Object]>
Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of
devices can be found in [lib/DeviceDescriptors.js](https://github.com/puppeteer/puppeteer/blob/master/lib/DeviceDescriptors.js).
devices can be found in [src/DeviceDescriptors.js](https://github.com/puppeteer/puppeteer/blob/master/src/DeviceDescriptors.ts).
```js
const puppeteer = require('puppeteer');
@ -520,7 +529,7 @@ try {
> **NOTE** The old way (Puppeteer versions <= v1.14.0) errors can be obtained with `require('puppeteer/Errors')`.
#### puppeteer.executablePath()
- returns: <[string]> A path where Puppeteer expects to find bundled Chromium. Chromium might not exist there if the download was skipped with [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables).
- returns: <[string]> A path where Puppeteer expects to find the bundled browser. The browser binary might not be there if the download was skipped with [`PUPPETEER_SKIP_DOWNLOAD`](#environment-variables).
> **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` and `PUPPETEER_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details.
@ -530,7 +539,7 @@ try {
- `product` <[string]> Which browser to launch. At this time, this is either `chrome` or `firefox`. See also `PUPPETEER_PRODUCT`.
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`.
- `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.compuppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
- `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
- `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `defaultViewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport.
- `width` <[number]> page width in pixels.
@ -570,17 +579,19 @@ const browser = await puppeteer.launch({
> 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/+/lkgr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users.
#### puppeteer.product
- returns: <[string]> returns the name of the browser that is under automation ("chrome" or "firefox")
- returns: <[string]> returns the name of the browser that is under automation (`"chrome"` or `"firefox"`)
The product is set by the `PUPPETEER_PRODUCT` environment variable or the `product` option in [puppeteer.launch([options])](#puppeteerlaunchoptions) and defaults to `chrome`. Firefox support is experimental.
### class: BrowserFetcher
BrowserFetcher can download and manage different versions of Chromium.
BrowserFetcher can download and manage different versions of Chromium and Firefox.
BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/).
In the Firefox case, BrowserFetcher downloads Firefox Nightly and operates on version numbers such as `"75"`.
An example of using BrowserFetcher to download a specific version of Chromium and running
Puppeteer against it:
@ -613,14 +624,20 @@ The method initiates a HEAD request to check if the revision is available.
The method initiates a GET request to download the revision from the host.
#### browserFetcher.host()
- returns: <[string]> The download host being used.
#### browserFetcher.localRevisions()
- returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk.
- returns: <[Promise]<[Array]<[string]>>> A list of all revisions (for the current `product`) available locally on disk.
#### browserFetcher.platform()
- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`.
#### browserFetcher.product()
- returns: <[string]> One of `chrome` or `firefox`.
#### browserFetcher.remove(revision)
- `revision` <[string]> a revision to remove. The method will throw if the revision has not been downloaded.
- `revision` <[string]> a revision to remove for the current `product`. The method will throw if the revision has not been downloaded.
- returns: <[Promise]> Resolves when the revision has been removed.
#### browserFetcher.revisionInfo(revision)
@ -631,6 +648,10 @@ The method initiates a GET request to download the revision from the host.
- `executablePath` <[string]> path to the revision executable
- `url` <[string]> URL this revision can be downloaded from
- `local` <[boolean]> whether the revision is locally available on disk
- `product` <[string]> one of `chrome` or `firefox`
> **NOTE** Many BrowserFetcher methods, like `remove` and `revisionInfo`
> are affected by the choice of `product`. See [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions).
### class: Browser
@ -1110,7 +1131,11 @@ If `pageFunction` returns a [Promise], then `page.$$eval` would wait for the pro
Examples:
```js
const divsCounts = await page.$$eval('div', divs => divs.length);
const divCount = await page.$$eval('div', divs => divs.length);
```
```js
const options = await page.$$eval('div > span.options', options => options.map(option => option.textContent));
```
#### page.$eval(selector, pageFunction[, ...args])
@ -1296,7 +1321,7 @@ const iPhone = puppeteer.devices['iPhone 6'];
})();
```
List of all available devices is available in the source code: [DeviceDescriptors.js](https://github.com/puppeteer/puppeteer/blob/master/lib/DeviceDescriptors.js).
List of all available devices is available in the source code: [src/DeviceDescriptors.ts](https://github.com/puppeteer/puppeteer/blob/master/src/DeviceDescriptors.ts).
#### page.emulateMedia(type)
- `type` <?[string]> Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation.
@ -1312,32 +1337,32 @@ List of all available devices is available in the source code: [DeviceDescriptor
```js
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]);
await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches);
// → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches);
// → false
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches);
// → false
await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]);
await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches));
await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches);
// → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches));
await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches);
// → false
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'dark' },
{ name: 'prefers-reduced-motion', value: 'reduce' },
]);
await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches);
// → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches);
// → false
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches));
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches);
// → false
await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches));
await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches);
// → true
await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches));
await page.evaluate(() => matchMedia('(prefers-reduced-motion: no-preference)').matches);
// → false
```
@ -1346,22 +1371,22 @@ await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').ma
- returns: <[Promise]>
```js
await page.evaluate(() => matchMedia('screen').matches));
// → true
await page.evaluate(() => matchMedia('print').matches));
await page.evaluate(() => matchMedia('screen').matches);
// → true
await page.evaluate(() => matchMedia('print').matches);
// → false
await page.emulateMediaType('print');
await page.evaluate(() => matchMedia('screen').matches));
await page.evaluate(() => matchMedia('screen').matches);
// → false
await page.evaluate(() => matchMedia('print').matches));
await page.evaluate(() => matchMedia('print').matches);
// → true
await page.emulateMediaType(null);
await page.evaluate(() => matchMedia('screen').matches));
// → true
await page.evaluate(() => matchMedia('print').matches));
await page.evaluate(() => matchMedia('screen').matches);
// → true
await page.evaluate(() => matchMedia('print').matches);
// → false
```
#### page.emulateTimezone(timezoneId)
@ -1531,7 +1556,7 @@ Shortcut for [page.mainFrame().focus(selector)](#framefocusselector).
#### page.goBack([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -1544,7 +1569,7 @@ Navigate to the previous page in history.
#### page.goForward([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -1558,7 +1583,7 @@ Navigate to the next page in history.
- `url` <[string]> URL to navigate page to. The url should include scheme, e.g. `https://`.
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -1655,13 +1680,13 @@ Page is guaranteed to have a main frame which persists during navigations.
> **NOTE** Generating a pdf is currently only supported in Chrome headless.
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call [page.emulateMedia('screen')](#pageemulatemediamediatype) before calling `page.pdf()`:
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call [page.emulateMediaType('screen')](#pageemulatemediatypetype) before calling `page.pdf()`:
> **NOTE** By default, `page.pdf()` generates a pdf with modified colors for printing. Use the [`-webkit-print-color-adjust`](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust) property to force rendering of exact colors.
```js
// Generates a PDF with 'screen' media type.
await page.emulateMedia('screen');
await page.emulateMediaType('screen');
await page.pdf({path: 'page.pdf'});
```
@ -1719,7 +1744,7 @@ Shortcut for [page.mainFrame().executionContext().queryObjects(prototypeHandle)]
#### page.reload([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -1777,7 +1802,7 @@ Toggles ignoring cache for each request based on the enabled state. By default,
- `html` <[string]> HTML markup to assign to the page.
- `options` <[Object]> Parameters which might have the following properties:
- `timeout` <[number]> Maximum time in milliseconds for resources to load, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider setting markup succeeded, defaults to `load`. Given an array of event strings, setting content is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider setting markup succeeded, defaults to `load`. Given an array of event strings, setting content is considered to be successful after all events have been fired. Events can be either:
- `load` - consider setting content to be finished when the `load` event is fired.
- `domcontentloaded` - consider setting content to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider setting content to be finished when there are no more than 0 network connections for at least `500` ms.
@ -2078,7 +2103,7 @@ Shortcut for [page.mainFrame().waitForFunction(pageFunction[, options[, ...args]
#### page.waitForNavigation([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -2781,7 +2806,7 @@ If there's no element matching `selector`, the method throws an error.
- `url` <[string]> URL to navigate frame to. The url should include scheme, e.g. `https://`.
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -2844,7 +2869,7 @@ frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections
- `html` <[string]> HTML markup to assign to the page.
- `options` <[Object]> Parameters which might have the following properties:
- `timeout` <[number]> Maximum time in milliseconds for resources to load, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider setting markup succeeded, defaults to `load`. Given an array of event strings, setting content is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider setting markup succeeded, defaults to `load`. Given an array of event strings, setting content is considered to be successful after all events have been fired. Events can be either:
- `load` - consider setting content to be finished when the `load` event is fired.
- `domcontentloaded` - consider setting content to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider setting content to be finished when there are no more than 0 network connections for at least `500` ms.
@ -2945,7 +2970,7 @@ await page.waitForFunction(selector => !!document.querySelector(selector), {}, s
#### frame.waitForNavigation([options])
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- `waitUntil` <[string]|[Array]<[string]>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|Array<PuppeteerLifeCycleMethod>> When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
- `load` - consider navigation to be finished when the `load` event is fired.
- `domcontentloaded` - consider navigation to be finished when the `DOMContentLoaded` event is fired.
- `networkidle0` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms.
@ -3706,6 +3731,9 @@ Contains the URL of the response.
#### securityDetails.protocol()
- returns: <[string]> String with the security protocol, eg. "TLS 1.2".
#### securityDetails.subjectAlternativeNames()
- returns: <[Array]<[string]>> Returns the list of SANs (subject alternative names) of the certificate.
#### securityDetails.subjectName()
- returns: <[string]> Name of the subject to which the certificate was issued to.
@ -3877,7 +3905,7 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer"
[CDPSession]: #class-cdpsession "CDPSession"
[ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess"
[ConnectionTransport]: ../lib/WebSocketTransport.js "ConnectionTransport"
[ConnectionTransport]: ../src/WebSocketTransport.js "ConnectionTransport"
[ConsoleMessage]: #class-consolemessage "ConsoleMessage"
[Coverage]: #class-coverage "Coverage"
[Dialog]: #class-dialog "Dialog"
@ -3903,7 +3931,7 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[Touchscreen]: #class-touchscreen "Touchscreen"
[Tracing]: #class-tracing "Tracing"
[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail"
[USKeyboardLayout]: ../lib/USKeyboardLayout.js "USKeyboardLayout"
[USKeyboardLayout]: ../src/USKeyboardLayout.ts "USKeyboardLayout"
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
[Worker]: #class-worker "Worker"
[boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean"

Просмотреть файл

@ -45,22 +45,27 @@ machine to check which dependencies are missing. The common ones are provided be
<summary>Debian (e.g. Ubuntu) Dependencies</summary>
```
ca-certificates
fonts-liberation
gconf-service
libappindicator1
libasound2
libatk1.0-0
libatk-bridge2.0-0
libatk1.0-0
libc6
libcairo2
libcups2
libdbus-1-3
libexpat1
libfontconfig1
libgbm1
libgcc1
libgconf-2-4
libgdk-pixbuf2.0-0
libglib2.0-0
libgtk-3-0
libnspr4
libnss3
libpango-1.0-0
libpangocairo-1.0-0
libstdc++6
@ -77,13 +82,9 @@ libxrandr2
libxrender1
libxss1
libxtst6
ca-certificates
fonts-liberation
libappindicator1
libnss3
lsb-release
xdg-utils
wget
xdg-utils
```
</details>
@ -91,27 +92,27 @@ wget
<summary>CentOS Dependencies</summary>
```
pango.x86_64
alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
GConf2.x86_64
gtk3.x86_64
ipa-gothic-fonts
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
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-utils
xorg-x11-fonts-cyrillic
xorg-x11-fonts-Type1
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils
```
After installing dependencies you need to update nss library using this command
@ -186,33 +187,20 @@ export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox
> 👋 We run our tests for Puppeteer on Travis CI - see our [`.travis.yml`](https://github.com/puppeteer/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)
- [xvfb](https://en.wikipedia.org/wiki/Xvfb) service should be launched in order to run Chromium in non-headless mode
- Runs on Xenial Linux on Travis by default
- Runs `npm install` by default
- `node_modules` is cached by default
To sum up, your `.travis.yml` might look like this:
`.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"
node_js: node
services: xvfb
script:
- npm run test
```
## Running Puppeteer on CircleCI
@ -249,7 +237,7 @@ Running Puppeteer smoothly on CircleCI requires the following steps:
## 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/puppeteer/puppeteer/blob/master/.ci/node8/Dockerfile.linux) for reference.
> 👋 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/puppeteer/puppeteer/blob/master/.ci/node10/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
@ -264,7 +252,9 @@ 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 - \
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& 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 fonts-freefont-ttf \
@ -338,7 +328,8 @@ RUN apk add --no-cache \
...
# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
# Puppeteer v1.19.0 works with Chromium 77.
RUN yarn add puppeteer@1.19.0
@ -355,14 +346,6 @@ 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.
@ -398,9 +381,9 @@ To use `puppeteer`, simply list the module as a dependency in your `package.json
### 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.
The Node.js 10 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.
To use `puppeteer`, simply list the module as a dependency in your `package.json` and deploy your function to Google Cloud Functions using the `nodejs10` runtime.
### Running Puppeteer on Heroku

Просмотреть файл

@ -32,6 +32,7 @@ More complex and use case driven examples can be found at [github.com/GoogleChro
- [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.
- [cucumber-puppeteer-example](https://github.com/mlampedx/cucumber-puppeteer-example) - Example repository demonstrating how to use Puppeeteer and Cucumber for integration testing.
## Services
- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps.

Просмотреть файл

@ -18,19 +18,16 @@
const puppeteer = require('puppeteer');
(async() => {
(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();
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 page.screenshot({ path: 'news.png', fullPage: true });
await browser.close();
})();

Просмотреть файл

@ -0,0 +1,48 @@
const puppeteer = require('puppeteer');
/**
* To have Puppeteer fetch a Firefox binary for you, first run:
*
* PUPPETEER_PRODUCT=firefox npm install
*
* To get additional logging about which browser binary is executed,
* run this example as:
*
* DEBUG=puppeteer:launcher NODE_PATH=../ node examples/cross-browser.js
*
* You can set a custom binary with the `executablePath` launcher option.
*
*
*/
const firefoxOptions = {
product: 'firefox',
extraPrefsFirefox: {
// Enable additional Firefox logging from its protocol implementation
// 'remote.log.level': 'Trace',
},
// Make browser logs visible
dumpio: true,
};
(async () => {
const browser = await puppeteer.launch(firefoxOptions);
const page = await browser.newPage();
console.log(await browser.version());
await page.goto('https://news.ycombinator.com/');
// Extract articles from the page.
const resultsSelector = '.storylink';
const links = await page.evaluate((resultsSelector) => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map((anchor) => {
const title = anchor.textContent.trim();
return `${title} - ${anchor.href}`;
});
}, resultsSelector);
console.log(links.join('\n'));
await browser.close();
})();

Просмотреть файл

@ -18,12 +18,12 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Define a window.onCustomEvent function on the page.
await page.exposeFunction('onCustomEvent', e => {
await page.exposeFunction('onCustomEvent', (e) => {
console.log(`${e.type} fired`, e.detail || '');
});
@ -33,16 +33,18 @@ const puppeteer = require('puppeteer');
* @return {!Promise}
*/
function listenFor(type) {
return page.evaluateOnNewDocument(type => {
document.addEventListener(type, e => {
window.onCustomEvent({type, detail: e.detail});
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 page.goto('https://www.chromestatus.com/features', {
waitUntil: 'networkidle0',
});
await browser.close();
})();

Просмотреть файл

@ -22,22 +22,22 @@ function sniffDetector() {
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
window.navigator.__defineGetter__('userAgent', function() {
window.navigator.__defineGetter__('userAgent', function () {
window.navigator.sniffed = true;
return userAgent;
});
window.navigator.__defineGetter__('platform', function() {
window.navigator.__defineGetter__('platform', function () {
window.navigator.sniffed = true;
return platform;
});
}
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.evaluateOnNewDocument(sniffDetector);
await page.goto('https://www.google.com', {waitUntil: 'networkidle2'});
await page.goto('https://www.google.com', { waitUntil: 'networkidle2' });
console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed)));
await browser.close();

Просмотреть файл

@ -18,15 +18,17 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
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'
format: 'letter',
});
await browser.close();

Просмотреть файл

@ -18,7 +18,7 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch({
// Launch chromium using a proxy server on port 9876.
// More on proxying:
@ -27,7 +27,7 @@ const puppeteer = require('puppeteer');
'--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');

Просмотреть файл

@ -19,11 +19,11 @@
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
(async() => {
(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 page.screenshot({ path: 'full.png', fullPage: true });
await browser.close();
})();

Просмотреть файл

@ -18,10 +18,10 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://example.com');
await page.screenshot({path: 'example.png'});
await page.screenshot({ path: 'example.png' });
await browser.close();
})();

Просмотреть файл

@ -23,7 +23,7 @@
const puppeteer = require('puppeteer');
(async() => {
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
@ -42,9 +42,9 @@ const puppeteer = require('puppeteer');
await page.waitForSelector(resultsSelector);
// Extract the results from the page.
const links = await page.evaluate(resultsSelector => {
const links = await page.evaluate((resultsSelector) => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => {
return anchors.map((anchor) => {
const title = anchor.textContent.split('|')[0].trim();
return `${title} - ${anchor.href}`;
});

Просмотреть файл

@ -14,21 +14,36 @@
* limitations under the License.
*/
const {helper} = require('./lib/helper');
const { helper } = require('./lib/helper');
const api = require('./lib/api');
const { Page } = require('./lib/Page');
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 = require('./lib/Puppeteer');
const packageJson = require('./package.json');
const preferredRevision = packageJson.puppeteer.chromium_revision;
const isPuppeteerCore = packageJson.name === 'puppeteer-core';
// Expose alias for deprecated method.
Page.prototype.emulateMedia = Page.prototype.emulateMediaType;
const puppeteer = new Puppeteer(__dirname, preferredRevision, isPuppeteerCore);
const { Puppeteer } = require('./lib/Puppeteer');
const packageJson = require('./package.json');
let preferredRevision = packageJson.puppeteer.chromium_revision;
const isPuppeteerCore = packageJson.name === 'puppeteer-core';
// puppeteer-core ignores environment variables
const product = isPuppeteerCore
? undefined
: process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
if (!isPuppeteerCore && product === 'firefox')
preferredRevision = packageJson.puppeteer.firefox_revision;
const puppeteer = new Puppeteer(
__dirname,
preferredRevision,
isPuppeteerCore,
product
);
// The introspection in `Helper.installAsyncStackHooks` references `Puppeteer._launcher`
// before the Puppeteer ctor is called, such that an invalid Launcher is selected at import,

Просмотреть файл

@ -14,114 +14,229 @@
* limitations under the License.
*/
// puppeteer-core should not install anything.
if (require('./package.json').name === 'puppeteer-core')
return;
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}
* This file is part of public API.
*
* By default, the `puppeteer` package runs this script during the installation
* process unless one of the env flags is provided.
* `puppeteer-core` package doesn't include this step at all. However, it's
* still possible to install a supported browser using this script when
* necessary.
*/
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);
}
const compileTypeScriptIfRequired = require('./typescript-if-required');
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 firefoxVersions =
'https://product-details.mozilla.org/1.0/firefox_versions.json';
const supportedProducts = {
chrome: 'Chromium',
firefox: 'Firefox Nightly',
};
async function download() {
await compileTypeScriptIfRequired();
const downloadHost =
process.env.PUPPETEER_DOWNLOAD_HOST ||
process.env.npm_config_puppeteer_download_host ||
process.env.npm_package_config_puppeteer_download_host;
const puppeteer = require('./index');
const product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product ||
'chrome';
const browserFetcher = puppeteer.createBrowserFetcher({
product,
host: downloadHost,
});
const revision = await getRevision();
await fetchBinary(revision);
function getRevision() {
if (product === 'chrome') {
return (
process.env.PUPPETEER_CHROMIUM_REVISION ||
process.env.npm_config_puppeteer_chromium_revision ||
process.env.npm_package_config_puppeteer_chromium_revision ||
require('./package.json').puppeteer.chromium_revision
);
} else if (product === 'firefox') {
puppeteer._preferredRevision = require('./package.json').puppeteer.firefox_revision;
return getFirefoxNightlyVersion(browserFetcher.host()).catch((error) => {
console.error(error);
process.exit(1);
});
} else {
throw new Error(`Unsupported product ${product}`);
}
}
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 fetchBinary(revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
function generateProtocolTypesIfNecessary(updated) {
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');
// Do nothing if the revision is already downloaded.
if (revisionInfo.local) {
logPolitely(
`${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.`
);
return;
}
// Override current environment proxy settings with npm configuration, if any.
const NPM_HTTPS_PROXY =
process.env.npm_config_https_proxy || process.env.npm_config_proxy;
const NPM_HTTP_PROXY =
process.env.npm_config_http_proxy || process.env.npm_config_proxy;
const NPM_NO_PROXY = process.env.npm_config_no_proxy;
if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY;
if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY;
/**
* @param {!Array<string>}
* @return {!Promise}
*/
function onSuccess(localRevisions) {
logPolitely(
`${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}`
);
localRevisions = localRevisions.filter(
(revision) => revision !== revisionInfo.revision
);
const cleanupOldVersions = localRevisions.map((revision) =>
browserFetcher.remove(revision)
);
Promise.all([...cleanupOldVersions]);
}
/**
* @param {!Error} error
*/
function onError(error) {
console.error(
`ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`
);
console.error(error);
process.exit(1);
}
let progressBar = null;
let lastDownloadedBytes = 0;
function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(
`Downloading ${
supportedProducts[product]
} r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}
return browserFetcher
.download(revisionInfo.revision, onProgress)
.then(() => browserFetcher.localRevisions())
.then(onSuccess)
.catch(onError);
}
function toMegabytes(bytes) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}
function getFirefoxNightlyVersion(host) {
const https = require('https');
const promise = new Promise((resolve, reject) => {
let data = '';
logPolitely(`Requesting latest Firefox Nightly version from ${host}`);
https
.get(firefoxVersions, (r) => {
if (r.statusCode >= 400)
return reject(new Error(`Got status code ${r.statusCode}`));
r.on('data', (chunk) => {
data += chunk;
});
r.on('end', () => {
try {
const versions = JSON.parse(data);
return resolve(versions.FIREFOX_NIGHTLY);
} catch {
return reject(new Error('Firefox version not found'));
}
});
})
.on('error', reject);
});
return promise;
}
}
function logPolitely(toBeLogged) {
const logLevel = process.env.npm_config_loglevel;
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
if (!logLevelDisplay)
console.log(toBeLogged);
if (!logLevelDisplay) console.log(toBeLogged);
}
if (process.env.PUPPETEER_SKIP_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.'
);
return;
}
if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.'
);
return;
}
download();

Просмотреть файл

@ -0,0 +1,68 @@
const mocha = require('mocha');
module.exports = JSONExtra;
const constants = mocha.Runner.constants;
/*
This is a copy of
https://github.com/mochajs/mocha/blob/master/lib/reporters/json-stream.js
with more event hooks. mocha does not support extending reporters or using
multiple reporters so a custom reporter is needed and it must be local
to the project.
*/
function JSONExtra(runner, options) {
mocha.reporters.Base.call(this, runner, options);
const self = this;
runner.once(constants.EVENT_RUN_BEGIN, function () {
writeEvent(['start', { total: runner.total }]);
});
runner.on(constants.EVENT_TEST_PASS, function (test) {
writeEvent(['pass', clean(test)]);
});
runner.on(constants.EVENT_TEST_FAIL, function (test, err) {
test = clean(test);
test.err = err.message;
test.stack = err.stack || null;
writeEvent(['fail', test]);
});
runner.once(constants.EVENT_RUN_END, function () {
writeEvent(['end', self.stats]);
});
runner.on(constants.EVENT_TEST_BEGIN, function (test) {
writeEvent(['test-start', clean(test)]);
});
runner.on(constants.EVENT_TEST_PENDING, function (test) {
writeEvent(['pending', clean(test)]);
});
}
function writeEvent(event) {
process.stdout.write(JSON.stringify(event) + '\n');
}
/**
* Returns an object literal representation of `test`
* free of cyclic properties, etc.
*
* @private
* @param {Test} test - Instance used as data source.
* @return {Object} object containing pared-down test instance data
*/
function clean(test) {
return {
title: test.title,
fullTitle: test.fullTitle(),
file: test.file,
duration: test.duration,
currentRetry: test.currentRetry(),
};
}

Просмотреть файл

@ -1,425 +0,0 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @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._hidden = 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;
if (property.name === 'hidden')
this._hidden = 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' || this._hidden)
return false;
if (this._focusable || this._richlyEditable)
return true;
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl())
return true;
// A non focusable child of a control is not interesting
if (insideControl)
return false;
return this.isLeafNode() && !!this._name;
}
/**
* @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};

Просмотреть файл

@ -1,383 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { helper, 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};

Просмотреть файл

@ -1,320 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
const util = require('util');
const extract = require('extract-zip');
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} */
let options = URL.parse(url);
options.method = method;
const proxyURL = getProxyForUrl(url);
if (proxyURL) {
if (url.startsWith('http:')) {
const proxy = URL.parse(proxyURL);
options = {
path: options.href,
host: proxy.hostname,
port: proxy.port,
};
} else {
/** @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
*/

Просмотреть файл

@ -1,242 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {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};

Просмотреть файл

@ -1,707 +0,0 @@
/**
* 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>>}
*/
async select(selector, ...values) {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const result = await handle.select(...values);
await handle.dispose();
return result;
}
/**
* @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};

Просмотреть файл

@ -1,872 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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': 'iPhone XR',
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
'viewport': {
'width': 414,
'height': 896,
'deviceScaleFactor': 3,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'iPhone XR landscape',
'userAgent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
'viewport': {
'width': 896,
'height': 414,
'deviceScaleFactor': 3,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'JioPhone 2',
'userAgent': 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
'viewport': {
'width': 240,
'height': 320,
'deviceScaleFactor': 1,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'JioPhone 2 landscape',
'userAgent': 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
'viewport': {
'width': 320,
'height': 240,
'deviceScaleFactor': 1,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Kindle Fire HDX',
'userAgent': 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
'viewport': {
'width': 800,
'height': 1280,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Kindle Fire HDX landscape',
'userAgent': 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
'viewport': {
'width': 1280,
'height': 800,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'LG Optimus L70',
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 384,
'height': 640,
'deviceScaleFactor': 1.25,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'LG Optimus L70 landscape',
'userAgent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 640,
'height': 384,
'deviceScaleFactor': 1.25,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Microsoft Lumia 550',
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
'viewport': {
'width': 640,
'height': 360,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Microsoft Lumia 950',
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
'viewport': {
'width': 360,
'height': 640,
'deviceScaleFactor': 4,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Microsoft Lumia 950 landscape',
'userAgent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
'viewport': {
'width': 640,
'height': 360,
'deviceScaleFactor': 4,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 10',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
'viewport': {
'width': 800,
'height': 1280,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 10 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
'viewport': {
'width': 1280,
'height': 800,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 4',
'userAgent': 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 384,
'height': 640,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 4 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 640,
'height': 384,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 5',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 360,
'height': 640,
'deviceScaleFactor': 3,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 5 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 640,
'height': 360,
'deviceScaleFactor': 3,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 5X',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 412,
'height': 732,
'deviceScaleFactor': 2.625,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 5X landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 732,
'height': 412,
'deviceScaleFactor': 2.625,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 6',
'userAgent': 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 412,
'height': 732,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 6 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 732,
'height': 412,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 6P',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 412,
'height': 732,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 6P landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 732,
'height': 412,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nexus 7',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
'viewport': {
'width': 600,
'height': 960,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nexus 7 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
'viewport': {
'width': 960,
'height': 600,
'deviceScaleFactor': 2,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nokia Lumia 520',
'userAgent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
'viewport': {
'width': 320,
'height': 533,
'deviceScaleFactor': 1.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nokia Lumia 520 landscape',
'userAgent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
'viewport': {
'width': 533,
'height': 320,
'deviceScaleFactor': 1.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Nokia N9',
'userAgent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
'viewport': {
'width': 480,
'height': 854,
'deviceScaleFactor': 1,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Nokia N9 landscape',
'userAgent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
'viewport': {
'width': 854,
'height': 480,
'deviceScaleFactor': 1,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Pixel 2',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 411,
'height': 731,
'deviceScaleFactor': 2.625,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Pixel 2 landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 731,
'height': 411,
'deviceScaleFactor': 2.625,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
},
{
'name': 'Pixel 2 XL',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 411,
'height': 823,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': false
}
},
{
'name': 'Pixel 2 XL landscape',
'userAgent': 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
'viewport': {
'width': 823,
'height': 411,
'deviceScaleFactor': 3.5,
'isMobile': true,
'hasTouch': true,
'isLandscape': true
}
}
];
for (const device of module.exports)
module.exports[device.name] = device;

Просмотреть файл

@ -1,203 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper, 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<*>}
*/
async evaluate(pageFunction, ...args) {
return await this._evaluateInternal(true /* returnByValue */, pageFunction, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {...*} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args);
}
/**
* @param {boolean} returnByValue
* @param {Function|string} pageFunction
* @param {...*} args
* @return {!Promise<*>}
*/
async _evaluateInternal(returnByValue, 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,
awaitPromise: true,
userGesture: true
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return returnByValue ? helper.valueFromRemoteObject(remoteObject) : 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,
awaitPromise: true,
userGesture: true
});
} catch (err) {
if (err instanceof TypeError && err.message.startsWith('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 returnByValue ? helper.valueFromRemoteObject(remoteObject) : 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.includes('Object reference chain is too long'))
return {result: {type: 'undefined'}};
if (error.message.includes('Object couldn\'t be returned by value'))
return {result: {type: 'undefined'}};
if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
}
/**
* @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};

Просмотреть файл

@ -1,717 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const EventEmitter = require('events');
const {helper, assert, debugError} = 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);
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,
}).catch(debugError))); // frames might be removed before we send this
}
/**
* @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};

Просмотреть файл

@ -1,567 +0,0 @@
/**
* 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 {Function|String} pageFunction
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>}
*/
async evaluate(pageFunction, ...args) {
return await this.executionContext().evaluate(pageFunction, this, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!Puppeteer.JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return await this.executionContext().evaluateHandle(pageFunction, this, ...args);
}
/**
* @param {string} propertyName
* @return {!Promise<?JSHandle>}
*/
async getProperty(propertyName) {
const objectHandle = await this.evaluateHandle((object, propertyName) => {
const result = {__proto__: null};
result[propertyName] = object[propertyName];
return result;
}, propertyName);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
return result;
}
/**
* @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.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._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>} values
* @return {!Promise<!Array<string>>}
*/
async select(...values) {
for (const value of values)
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
return this.evaluate((element, values) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}, values);
}
/**
* @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.evaluate(element => element.focus());
}
/**
* @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.evaluateHandle(
(element, selector) => element.querySelector(selector),
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.evaluateHandle(
(element, selector) => element.querySelectorAll(selector),
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 elementHandle.evaluate(pageFunction, ...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.evaluateHandle(
(element, selector) => Array.from(element.querySelectorAll(selector)),
selector
);
const result = await arrayHandle.evaluate(pageFunction, ...args);
await arrayHandle.dispose();
return result;
}
/**
* @param {string} expression
* @return {!Promise<!Array<!ElementHandle>>}
*/
async $x(expression) {
const arrayHandle = await this.evaluateHandle(
(element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
const array = [];
let item;
while ((item = iterator.iterateNext()))
array.push(item);
return array;
},
expression
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle)
result.push(elementHandle);
}
return result;
}
/**
* @returns {!Promise<boolean>}
*/
isIntersectingViewport() {
return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
return visibleRatio > 0;
});
}
}
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};

Просмотреть файл

@ -1,905 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const 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 debugLauncher = require('debug')(`puppeteer:launcher`);
const {TimeoutError} = require('./Errors');
const WebSocketTransport = require('./WebSocketTransport');
const PipeTransport = require('./PipeTransport');
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const removeFolderAsync = helper.promisify(removeFolder);
const writeFileAsync = helper.promisify(fs.writeFile);
class BrowserRunner {
/**
* @param {string} executablePath
* @param {!Array<string>} processArguments
* @param {string=} tempDirectory
*/
constructor(executablePath, processArguments, tempDirectory) {
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
this.proc = null;
this.connection = null;
this._closed = true;
this._listeners = [];
}
/**
* @param {!(Launcher.LaunchOptions)=} options
*/
start(options = {}) {
const {
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
dumpio,
env,
pipe
} = options;
/** @type {!Array<"ignore"|"pipe">} */
let stdio = ['pipe', 'pipe', 'pipe'];
if (pipe) {
if (dumpio)
stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else
stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
assert(!this.proc, 'This process has previously been started.');
debugLauncher(`Calling ${this._executablePath} ${this._processArguments.join(' ')}`);
this.proc = childProcess.spawn(
this._executablePath,
this._processArguments,
{
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio
}
);
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill, reject) => {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch(err => console.error(err));
} else {
fulfill();
}
});
});
this._listeners = [ helper.addEventListener(process, 'exit', this.kill.bind(this)) ];
if (handleSIGINT)
this._listeners.push(helper.addEventListener(process, 'SIGINT', () => { this.kill(); process.exit(130); }));
if (handleSIGTERM)
this._listeners.push(helper.addEventListener(process, 'SIGTERM', this.close.bind(this)));
if (handleSIGHUP)
this._listeners.push(helper.addEventListener(process, 'SIGHUP', this.close.bind(this)));
}
/**
* @return {Promise}
*/
close() {
if (this._closed)
return Promise.resolve();
helper.removeEventListeners(this._listeners);
if (this._tempDirectory) {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
this.connection.send('Browser.close').catch(error => {
debugError(error);
this.kill();
});
}
return this._processClosing;
}
// This function has to be sync to be used as 'exit' event handler.
kill() {
helper.removeEventListeners(this._listeners);
if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
else
process.kill(-this.proc.pid, 'SIGKILL');
} catch (error) {
// the process might have already stopped
}
}
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(this._tempDirectory);
} catch (error) { }
}
/**
* @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
*
* @return {!Promise<!Connection>}
*/
async setupConnection(options) {
const {
usePipe,
timeout,
slowMo,
preferredRevision
} = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(this.proc, timeout, preferredRevision);
const transport = await WebSocketTransport.create(browserWSEndpoint);
this.connection = new Connection(browserWSEndpoint, transport, slowMo);
} else {
const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(this.proc.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (this.proc.stdio[4]));
this.connection = new Connection('', transport, slowMo);
}
return this.connection;
}
}
/**
* @implements {!Puppeteer.ProductLauncher}
*/
class ChromeLauncher {
/**
* @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 profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
const chromeArguments = [];
if (!ignoreDefaultArgs)
chromeArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
chromeArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
else
chromeArguments.push(...args);
let temporaryUserDataDir = null;
if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(profilePath);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
}
let chromeExecutable = executablePath;
if (!executablePath) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner(chromeExecutable, chromeArguments, temporaryUserDataDir);
runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe: usePipe});
try {
const connection = await runner.setupConnection({usePipe, timeout, slowMo, preferredRevision: this._preferredRevision});
const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget(t => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
throw error;
}
}
/**
* @param {!Launcher.ChromeArgOptions=} options
* @return {!Array<string>}
*/
defaultArgs(options = {}) {
const chromeArguments = [
'--disable-background-networking',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=TranslateUI',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null
} = options;
if (userDataDir)
chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (headless) {
chromeArguments.push(
'--headless',
'--hide-scrollbars',
'--mute-audio'
);
}
if (args.every(arg => arg.startsWith('-')))
chromeArguments.push('about:blank');
chromeArguments.push(...args);
return chromeArguments;
}
/**
* @return {string}
*/
executablePath() {
return resolveExecutablePath(this).executablePath;
}
/**
* @return {string}
*/
get product() {
return 'chrome';
}
/**
* @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));
}
}
/**
* @implements {!Puppeteer.ProductLauncher}
*/
class FirefoxLauncher {
/**
* @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 & {extraPrefsFirefox?: !object})=} 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,
extraPrefsFirefox = {}
} = options;
const firefoxArguments = [];
if (!ignoreDefaultArgs)
firefoxArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
firefoxArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
else
firefoxArguments.push(...args);
if (!firefoxArguments.some(argument => argument.startsWith('--remote-debugging-')))
firefoxArguments.push('--remote-debugging-port=0');
let temporaryUserDataDir = null;
if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
}
let executable = executablePath;
if (!executablePath) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText)
throw new Error(missingText);
executable = executablePath;
}
const runner = new BrowserRunner(executable, firefoxArguments, temporaryUserDataDir);
runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe});
try {
const connection = await runner.setupConnection({usePipe: pipe, timeout, slowMo, preferredRevision: this._preferredRevision});
const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
await browser.waitForTarget(t => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
throw error;
}
}
/**
* @param {!(Launcher.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 {string}
*/
executablePath() {
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
// TODO get resolveExecutablePath working for Firefox
if (!executablePath)
throw new Error('Please set PUPPETEER_EXECUTABLE_PATH to a Firefox binary.');
return executablePath;
}
/**
* @return {string}
*/
get product() {
return 'firefox';
}
/**
* @param {!Launcher.ChromeArgOptions=} options
* @return {!Array<string>}
*/
defaultArgs(options = {}) {
const firefoxArguments = [
'--no-remote',
'--foreground',
];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null
} = options;
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
if (headless)
firefoxArguments.push('--headless');
if (devtools)
firefoxArguments.push('--devtools');
if (args.every(arg => arg.startsWith('-')))
firefoxArguments.push('about:blank');
firefoxArguments.push(...args);
return firefoxArguments;
}
/**
* @param {!Object=} extraPrefs
* @return {!Promise<string>}
*/
async _createProfile(extraPrefs) {
const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-'));
const prefsJS = [];
const userJS = [];
const server = 'dummy.test';
const defaultPreferences = {
// Make sure Shield doesn't hit the network.
'app.normandy.api_url': '',
// Disable Firefox old build background check
'app.update.checkInstallTime': false,
// Disable automatically upgrading Firefox
'app.update.disabledForTesting': true,
// Increase the APZ content response timeout to 1 minute
'apz.content_response_timeout': 60000,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
// Enable the dump function: which sends messages to the system
// console
// https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
'browser.dom.window.dump.enabled': true,
// Disable topstories
'browser.newtabpage.activity-stream.feeds.section.topstories': false,
// Always display a blank page
'browser.newtabpage.enabled': false,
// Background thumbnails in particular cause grief: and disabling
// thumbnails in general cannot hurt
'browser.pagethumbnails.capturing_disabled': true,
// Disable safebrowsing components.
'browser.safebrowsing.blockedURIs.enabled': false,
'browser.safebrowsing.downloads.enabled': false,
'browser.safebrowsing.malware.enabled': false,
'browser.safebrowsing.passwords.enabled': false,
'browser.safebrowsing.phishing.enabled': false,
// Disable updates to search engines.
'browser.search.update': false,
// Do not restore the last open set of tabs if the browser has crashed
'browser.sessionstore.resume_from_crash': false,
// Skip check for default browser on startup
'browser.shell.checkDefaultBrowser': false,
// Disable newtabpage
'browser.startup.homepage': 'about:blank',
// Do not redirect user when a milstone upgrade of Firefox is detected
'browser.startup.homepage_override.mstone': 'ignore',
// Start with a blank page about:blank
'browser.startup.page': 0,
// Do not allow background tabs to be zombified on Android: otherwise for
// tests that open additional tabs: the test harness tab itself might get
// unloaded
'browser.tabs.disableBackgroundZombification': false,
// Do not warn when closing all other open tabs
'browser.tabs.warnOnCloseOtherTabs': false,
// Do not warn when multiple tabs will be opened
'browser.tabs.warnOnOpen': false,
// Disable the UI tour.
'browser.uitour.enabled': false,
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
'browser.urlbar.suggest.searches': false,
// Disable first run splash page on Windows 10
'browser.usedOnWindows10.introURL': '',
// Do not warn on quitting Firefox
'browser.warnOnQuit': false,
// Do not show datareporting policy notifications which can
// interfere with tests
'datareporting.healthreport.about.reportUrl': `http://${server}/dummy/abouthealthreport/`,
'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
'datareporting.healthreport.logging.consoleEnabled': false,
'datareporting.healthreport.service.enabled': false,
'datareporting.healthreport.service.firstRun': false,
'datareporting.healthreport.uploadEnabled': false,
'datareporting.policy.dataSubmissionEnabled': false,
'datareporting.policy.dataSubmissionPolicyAccepted': false,
'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
// This doesn't affect Puppeteer but spams console (Bug 1424372)
'devtools.jsonview.enabled': false,
// Disable popup-blocker
'dom.disable_open_during_load': false,
// Enable the support for File object creation in the content process
// Required for |Page.setFileInputFiles| protocol method.
'dom.file.createInChild': true,
// Disable the ProcessHangMonitor
'dom.ipc.reportProcessHangs': false,
// Disable slow script dialogues
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
'extensions.enabledScopes': 5,
// Disable metadata caching for installed add-ons by default
'extensions.getAddons.cache.enabled': false,
// Disable installing any distribution extensions or add-ons.
'extensions.installDistroAddons': false,
// Disabled screenshots extension
'extensions.screenshots.disabled': true,
// Turn off extension updates so they do not bother tests
'extensions.update.enabled': false,
// Turn off extension updates so they do not bother tests
'extensions.update.notifyUser': false,
// Make sure opening about:addons will not hit the network
'extensions.getAddons.discovery.api_url': 'data:, ',
// Allow the application to have focus even it runs in the background
'focusmanager.testmode': true,
// Disable useragent updates
'general.useragent.updates.enabled': false,
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
'geo.provider.testing': true,
// Do not scan Wifi
'geo.wifi.scan': false,
// Force sRGB tagged screenshots for image encoder
// https://bugzilla.mozilla.org/show_bug.cgi?id=1615395
'gfx.color_management.force_srgb': true,
// No hang monitor
'hangmonitor.timeout': 0,
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'network.cookie.cookieBehavior': 0,
// Do not prompt for temporary redirects
'network.http.prompt-temp-redirect': false,
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
'network.http.speculative-parallel-limit': 0,
// Do not automatically switch between offline and online
'network.manage-offline-status': false,
// Make sure SNTP requests do not hit the network
'network.sntp.pools': server,
// Disable Flash.
'plugin.state.flash': 0,
'privacy.trackingprotection.enabled': false,
// Enable Remote Agent
// https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
'remote.enabled': true,
// Don't do network connections for mitm priming
'security.certerrors.mitm.priming.enabled': false,
// Local documents have access to all other local documents,
// including directory listings
'security.fileuri.strict_origin_policy': false,
// Do not wait for the notification button security delay
'security.notification_enable_delay': 0,
// Ensure blocklist updates do not hit the network
'services.settings.server': `http://${server}/dummy/blocklist/`,
// Do not automatically fill sign-in forms with known usernames and
// passwords
'signon.autofillForms': false,
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
'signon.rememberSignons': false,
// Disable first-run welcome page
'startup.homepage_welcome_url': 'about:blank',
// Disable first-run welcome page
'startup.homepage_welcome_url.additional': '',
// Disable browser animations (tabs, fullscreen, sliding alerts)
'toolkit.cosmeticAnimations.enabled': false,
// We want to collect telemetry, but we don't want to send in the results
'toolkit.telemetry.server': `https://${server}/dummy/telemetry/`,
// Prevent starting into safe mode after application crashes
'toolkit.startup.max_resumed_crashes': -1,
};
Object.assign(defaultPreferences, extraPrefs);
for (const [key, value] of Object.entries(defaultPreferences))
userJS.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
await writeFileAsync(path.join(profilePath, 'prefs.js'), prefsJS.join('\n'));
return profilePath;
}
}
/**
* @param {!Puppeteer.ChildProcess} browserProcess
* @param {number} timeout
* @param {string} preferredRevision
* @return {!Promise<string>}
*/
function waitForWSEndpoint(browserProcess, timeout, preferredRevision) {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper.addEventListener(browserProcess, 'error', error => onClose(error))
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
/**
* @param {!Error=} error
*/
function onClose(error) {
cleanup();
reject(new Error([
'Failed to launch the browser process!' + (error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')));
}
function onTimeout() {
cleanup();
reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`));
}
/**
* @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;
});
}
/**
* @param {ChromeLauncher|FirefoxLauncher} launcher
*
* @return {{executablePath: string, missingText: ?string}}
*/
function resolveExecutablePath(launcher) {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!launcher._isPuppeteerCore) {
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
if (executablePath) {
const missingText = !fs.existsSync(executablePath) ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + executablePath : null;
return { executablePath, missingText };
}
}
const browserFetcher = new BrowserFetcher(launcher._projectRoot);
if (!launcher._isPuppeteerCore) {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
return {executablePath: revisionInfo.executablePath, missingText};
}
}
const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
const missingText = !revisionInfo.local ? `Browser is not downloaded. Run "npm install" or "yarn install"` : null;
return {executablePath: revisionInfo.executablePath, missingText};
}
/**
* @param {string} projectRoot
* @param {string} preferredRevision
* @param {boolean} isPuppeteerCore
* @param {string=} product
* @return {!Puppeteer.ProductLauncher}
*/
function Launcher(projectRoot, preferredRevision, isPuppeteerCore, product) {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore)
product = process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product;
switch (product) {
case 'firefox':
return new FirefoxLauncher(projectRoot, preferredRevision, isPuppeteerCore);
case 'chrome':
default:
return new ChromeLauncher(projectRoot, preferredRevision, isPuppeteerCore);
}
}
/**
* @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;

Просмотреть файл

@ -1,198 +0,0 @@
/**
* 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.get(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 of ' + 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 = new Map([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);
module.exports = {LifecycleWatcher};

Просмотреть файл

@ -1,136 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @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;

Просмотреть файл

@ -1,794 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const EventEmitter = require('events');
const {helper, assert, debugError} = require('./helper');
const {Events} = require('./Events');
class NetworkManager extends EventEmitter {
/**
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.FrameManager} frameManager
*/
constructor(client, ignoreHTTPSErrors, frameManager) {
super();
this._client = client;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._frameManager = frameManager;
/** @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 {?{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.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,
responsePhrase: STATUS_TEXTS[response.status || 200],
responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined,
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
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) {
if (!Object.is(headers[name], undefined))
result.push({name, value: headers[name] + ''});
}
return result;
}
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
const STATUS_TEXTS = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
};
module.exports = {Request, Response, NetworkManager, SecurityDetails};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,105 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Launcher = 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._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
/**
* @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions & {product?: string, extraPrefsFirefox?: !object})=} options
* @return {!Promise<!Puppeteer.Browser>}
*/
launch(options) {
if (!this._productName && options)
this._productName = options.product;
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 {!Puppeteer.ProductLauncher}
*/
get _launcher() {
if (!this._lazyLauncher)
this._lazyLauncher = Launcher(this._projectRoot, this._preferredRevision, this._isPuppeteerCore, this._productName);
return this._lazyLauncher;
}
/**
* @return {string}
*/
get product() {
return this._launcher.product;
}
/**
* @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);
}
};

Просмотреть файл

@ -1,148 +0,0 @@
/**
* 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');
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) {
// TODO(einbinder): Make workers send their console logs.
this._workerPromise = this._sessionFactory()
.then(client => new Worker(client, 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};

Просмотреть файл

@ -1,17 +0,0 @@
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};

Просмотреть файл

@ -1,72 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper, assert} = require('./helper');
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 => {
helper.readProtocolStream(this._client, event.stream, this._path).then(fulfill);
});
await this._client.send('Tracing.end');
this._recording = false;
return contentPromise;
}
}
module.exports = Tracing;

Просмотреть файл

@ -1,288 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @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'},
'SoftLeft': {'key': 'SoftLeft', 'code': 'SoftLeft', 'location': 4},
'SoftRight': {'key': 'SoftRight', 'code': 'SoftRight', 'location': 4},
'Camera': {'keyCode': 44, 'key': 'Camera', 'code': 'Camera', 'location': 4},
'Call': {'key': 'Call', 'code': 'Call', 'location': 4},
'EndCall': {'keyCode': 95, 'key': 'EndCall', 'code': 'EndCall', 'location': 4},
'VolumeDown': {'keyCode': 182, 'key': 'VolumeDown', 'code': 'VolumeDown', 'location': 4},
'VolumeUp': {'keyCode': 183, 'key': 'VolumeUp', 'code': 'VolumeUp', 'location': 4},
};

Просмотреть файл

@ -1,80 +0,0 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const EventEmitter = require('events');
const {debugError} = require('./helper');
const {ExecutionContext} = require('./ExecutionContext');
const {JSHandle} = require('./JSHandle');
class Worker extends EventEmitter {
/**
* @param {Puppeteer.CDPSession} client
* @param {string} url
* @param {function(string, !Array<!JSHandle>, Protocol.Runtime.StackTrace=):void} consoleAPICalled
* @param {function(!Protocol.Runtime.ExceptionDetails):void} exceptionThrown
*/
constructor(client, url, consoleAPICalled, exceptionThrown) {
super();
this._client = client;
this._url = url;
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
/** @type {function(!Protocol.Runtime.RemoteObject):!JSHandle} */
let jsHandleFactory;
this._client.once('Runtime.executionContextCreated', async event => {
jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(client, event.context, null);
this._executionContextCallback(executionContext);
});
// This might fail if the target is closed before we recieve all execution contexts.
this._client.send('Runtime.enable', {}).catch(debugError);
this._client.on('Runtime.consoleAPICalled', event => consoleAPICalled(event.type, event.args.map(jsHandleFactory), event.stackTrace));
this._client.on('Runtime.exceptionThrown', exception => exceptionThrown(exception.exceptionDetails));
}
/**
* @return {string}
*/
url() {
return this._url;
}
/**
* @return {!Promise<ExecutionContext>}
*/
async executionContext() {
return this._executionContextPromise;
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
async evaluate(pageFunction, ...args) {
return (await this._executionContextPromise).evaluate(pageFunction, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args);
}
}
module.exports = {Worker};

66
remote/test/puppeteer/lib/externs.d.ts поставляемый
Просмотреть файл

@ -1,66 +0,0 @@
import { Connection as RealConnection, CDPSession as RealCDPSession } from './Connection.js';
import { Browser as RealBrowser, BrowserContext as RealBrowserContext} from './Browser.js';
import {Target as RealTarget} from './Target.js';
import {Page as RealPage} from './Page.js';
import {TaskQueue as RealTaskQueue} from './TaskQueue.js';
import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js';
import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js';
import {JSHandle as RealJSHandle, ElementHandle as RealElementHandle} from './JSHandle.js';
import {DOMWorld as RealDOMWorld} from './DOMWorld.js';
import {TimeoutSettings as RealTimeoutSettings} from './TimeoutSettings.js';
import {ExecutionContext as RealExecutionContext} from './ExecutionContext.js';
import { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js';
import * as child_process from 'child_process';
declare global {
module Puppeteer {
export class Connection extends RealConnection {}
export class CDPSession extends RealCDPSession {
on<T extends keyof Protocol.Events>(event: T, listener: (arg: Protocol.Events[T]) => void): this;
send<T extends keyof Protocol.CommandParameters>(message: T, parameters?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T]>;
}
export class Mouse extends RealMouse {}
export class Keyboard extends RealKeyboard {}
export class Touchscreen extends RealTouchscreen {}
export class TaskQueue extends RealTaskQueue {}
export class Browser extends RealBrowser {}
export class BrowserContext extends RealBrowserContext {}
export class Target extends RealTarget {}
export class Frame extends RealFrame {}
export class FrameManager extends RealFrameManager {}
export class NetworkManager extends RealNetworkManager {}
export class ElementHandle extends RealElementHandle {}
export class JSHandle extends RealJSHandle {}
export class DOMWorld extends RealDOMWorld {}
export class TimeoutSettings extends RealTimeoutSettings {}
export class ExecutionContext extends RealExecutionContext {}
export class Page extends RealPage { }
export class Response extends RealResponse { }
export class Request extends RealRequest { }
export interface ConnectionTransport {
send(string);
close();
onmessage?: (message: string) => void,
onclose?: () => void,
}
export interface ProductLauncher {
launch(object)
connect(object)
executablePath: () => string,
defaultArgs(object)
product:string,
}
export interface ChildProcess extends child_process.ChildProcess { }
export type Viewport = {
width: number;
height: number;
deviceScaleFactor?: number;
isMobile?: boolean;
isLandscape?: boolean;
hasTouch?: boolean;
}
}
}

Просмотреть файл

@ -1,290 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TimeoutError} = require('./Errors');
const debugError = require('debug')(`puppeteer:error`);
const fs = require('fs');
class Helper {
/**
* @param {Function|string} fun
* @param {!Array<*>} args
* @return {string}
*/
static evaluationString(fun, ...args) {
if (Helper.isString(fun)) {
assert(args.length === 0, '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);
}
}
/**
* @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
* @return {string}
*/
static getExceptionMessage(exceptionDetails) {
if (exceptionDetails.exception)
return exceptionDetails.exception.description || exceptionDetails.exception.value;
let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) {
const location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
const functionName = callframe.functionName || '<anonymous>';
message += `\n at ${functionName} (${location})`;
}
}
return message;
}
/**
* @param {!Protocol.Runtime.RemoteObject} remoteObject
* @return {*}
*/
static valueFromRemoteObject(remoteObject) {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
return BigInt(remoteObject.unserializableValue.replace('n', ''));
switch (remoteObject.unserializableValue) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
}
}
return remoteObject.value;
}
/**
* @param {!Puppeteer.CDPSession} client
* @param {!Protocol.Runtime.RemoteObject} remoteObject
*/
static async releaseObject(client, remoteObject) {
if (!remoteObject.objectId)
return;
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}
/**
* @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 = {};
Error.captureStackTrace(syncStack);
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 {!NodeJS.EventEmitter} emitter
* @param {(string|symbol)} eventName
* @param {function(?):void} 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(?):void}>} listeners
*/
static removeEventListeners(listeners) {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.length = 0;
}
/**
* @param {!Object} obj
* @return {boolean}
*/
static isString(obj) {
return typeof obj === 'string' || obj instanceof String;
}
/**
* @param {!Object} obj
* @return {boolean}
*/
static isNumber(obj) {
return typeof obj === 'number' || obj instanceof Number;
}
/**
* @param {function} nodeFunction
* @return {function}
*/
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 {!NodeJS.EventEmitter} emitter
* @param {(string|symbol)} eventName
* @param {function} predicate
* @param {number} timeout
* @param {!Promise<!Error>} abortPromise
* @return {!Promise}
*/
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
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;
resolveCallback(event);
});
if (timeout) {
eventTimeout = setTimeout(() => {
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
}, timeout);
}
function cleanup() {
Helper.removeEventListeners([listener]);
clearTimeout(eventTimeout);
}
const result = await Promise.race([promise, abortPromise]).then(r => {
cleanup();
return r;
}, e => {
cleanup();
throw e;
});
if (result instanceof Error)
throw result;
return result;
}
/**
* @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);
let timeoutTimer = null;
if (timeout)
timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutTimer)
clearTimeout(timeoutTimer);
}
}
/**
* @param {!Puppeteer.CDPSession} client
* @param {string} handle
* @param {?string} path
* @return {!Promise<!Buffer>}
*/
static async readProtocolStream(client, handle, path) {
let eof = false;
let file;
if (path)
file = await openAsync(path, 'w');
const bufs = [];
while (!eof) {
const response = await client.send('IO.read', {handle});
eof = response.eof;
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
bufs.push(buf);
if (path)
await writeAsync(file, buf);
}
if (path)
await closeAsync(file);
await client.send('IO.close', {handle});
let resultBuffer = null;
try {
resultBuffer = Buffer.concat(bufs);
} finally {
return resultBuffer;
}
}
}
const openAsync = Helper.promisify(fs.open);
const writeAsync = Helper.promisify(fs.write);
const closeAsync = Helper.promisify(fs.close);
/**
* @param {*} value
* @param {string=} message
*/
function assert(value, message) {
if (!value)
throw new Error(message);
}
module.exports = {
helper: Helper,
assert,
debugError
};

Просмотреть файл

@ -0,0 +1,4 @@
module.exports = {
reporter: 'dot',
exit: !!process.env.CI,
};

Просмотреть файл

@ -0,0 +1,6 @@
const base = require('./base');
module.exports = {
...base,
spec: 'test/assert-coverage-test.js',
};

Просмотреть файл

@ -0,0 +1,6 @@
const base = require('./base');
module.exports = {
...base,
spec: 'utils/doclint/**/*.spec.js',
};

Просмотреть файл

@ -1,5 +1,5 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,19 +14,13 @@
* limitations under the License.
*/
const {TestRunner, Reporter} = require('..');
const base = require('./base');
const runner = new TestRunner({ timeout: 100 });
const reporter = new Reporter(runner);
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
it('timeout', async (state) => {
await new Promise(() => {});
});
});
runner.run();
module.exports = {
...base,
file: ['./test/mocha-utils.js'],
spec: 'test/*.spec.js',
// retry twice more, so we run each test up to 3 times if needed.
retries: 2,
timeout: 25 * 1000,
};

Просмотреть файл

@ -5,6 +5,6 @@ origin:
description: Headless Chrome Node API
license: Apache-2.0
name: puppeteer
release: firefox
url: https://github.com/andreastt/puppeteer.git
release: 8ba36752e735b245baf4915cff341d2b85a83161
url: https://github.com/mjzffr/puppeteer.git
schema: 1

Просмотреть файл

@ -1,62 +1,90 @@
{
"name": "puppeteer",
"version": "2.0.0-post",
"version": "3.1.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"main": "index.js",
"repository": "github:puppeteer/puppeteer",
"engines": {
"node": ">=8.16.0"
"node": ">=10.18.1"
},
"puppeteer": {
"chromium_revision": "706915"
"chromium_revision": "756035",
"firefox_revision": "latest"
},
"scripts": {
"unit": "node test/test.js",
"fjunit": "PUPPETEER_PRODUCT=juggler node test/test.js",
"funit": "PUPPETEER_PRODUCT=firefox node test/test.js",
"unit": "mocha --config mocha-config/puppeteer-unit-tests.js",
"unit-with-coverage": "cross-env COVERAGE=1 npm run unit",
"assert-unit-coverage": "cross-env COVERAGE=1 mocha --config mocha-config/coverage-tests.js",
"funit": "PUPPETEER_PRODUCT=firefox npm run unit",
"debug-unit": "node --inspect-brk test/test.js",
"test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js",
"test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types && node utils/testrunner/test/test.js",
"test-doclint": "mocha --config mocha-config/doclint-tests.js",
"test": "npm run tsc && npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types",
"prepare": "node typescript-if-required.js",
"prepublishOnly": "npm run tsc",
"dev-install": "npm run tsc && node install.js",
"install": "node install.js",
"lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run tsc && npm run doc",
"eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"eslint-fix": "eslint --ext js --ext ts --fix .",
"lint": "npm run eslint && npm run tsc && npm run doc",
"doc": "node utils/doclint/cli.js",
"coverage": "cross-env COVERAGE=true npm run unit",
"tsc": "tsc -p .",
"tsc": "tsc --version && tsc -p . && cp src/protocol.d.ts lib/",
"apply-next-version": "node utils/apply_next_version.js",
"bundle": "npx browserify -r ./index.js:puppeteer -o utils/browser/puppeteer-web.js",
"test-types": "node utils/doclint/generate_types && npx -p typescript@2.1 tsc -p utils/doclint/generate_types/test/",
"unit-bundle": "node utils/browser/test.js"
"test-types": "node utils/doclint/generate_types && tsc --version && tsc -p utils/doclint/generate_types/test/",
"update-protocol-d-ts": "node utils/protocol-types-generator update",
"compare-protocol-d-ts": "node utils/protocol-types-generator compare",
"test-install": "scripts/test-install.sh"
},
"files": [
"lib/",
"Errors.js",
"DeviceDescriptors.js",
"index.js",
"install.js",
"typescript-if-required.js"
],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0",
"extract-zip": "^1.6.6",
"https-proxy-agent": "^3.0.0",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"mime": "^2.0.3",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^2.6.1",
"ws": "^6.1.0"
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
},
"devDependencies": {
"@types/debug": "0.0.31",
"@types/extract-zip": "^1.6.2",
"@types/mime": "^2.0.0",
"@types/node": "^8.10.34",
"@types/node": "^10.17.14",
"@types/proxy-from-env": "^1.0.1",
"@types/rimraf": "^2.0.2",
"@types/ws": "^6.0.1",
"@types/tar-fs": "^1.16.2",
"@types/ws": "^7.2.4",
"@typescript-eslint/eslint-plugin": "^2.28.0",
"@typescript-eslint/parser": "^2.28.0",
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"eslint": "^5.15.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-unicorn": "^19.0.1",
"esprima": "^4.0.0",
"jpeg-js": "^0.3.4",
"expect": "^25.2.7",
"jpeg-js": "^0.3.7",
"minimist": "^1.2.0",
"mocha": "^7.1.1",
"ncp": "^2.0.0",
"pixelmatch": "^4.0.2",
"pngjs": "^3.3.3",
"pngjs": "^5.0.0",
"prettier": "^2.0.5",
"sinon": "^9.0.2",
"text-diff": "^1.0.1",
"typescript": "3.2.2"
"typescript": "3.9.2"
},
"browser": {
"./lib/BrowserFetcher.js": false,

Просмотреть файл

@ -0,0 +1,5 @@
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
};

Просмотреть файл

@ -0,0 +1,15 @@
#!/usr/bin/env sh
set -e
# Pack the module into a tarball
npm pack
tarball="$(realpath puppeteer-*.tgz)"
cd "$(mktemp -d)"
# Check we can install from the tarball.
# This emulates installing from npm and ensures that:
# 1. we publish the right files in the `files` list from package.json
# 2. The install script works and correctly exits without errors
# 3. Requiring Puppeteer from Node works.
npm install --loglevel silent "${tarball}"
node --eval="require('puppeteer')"
rm "${tarball}"

Просмотреть файл

@ -0,0 +1,381 @@
/**
* 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.
*/
// Used as a TypeDef
// eslint-disable-next-line no-unused-vars
import { CDPSession } from './Connection';
// Used as a TypeDef
// eslint-disable-next-line no-unused-vars
import { ElementHandle } from './JSHandle';
interface SerializedAXNode {
role: string;
name?: string;
value?: string | number;
description?: string;
keyshortcuts?: string;
roledescription?: string;
valuetext?: string;
disabled?: boolean;
expanded?: boolean;
focused?: boolean;
modal?: boolean;
multiline?: boolean;
multiselectable?: boolean;
readonly?: boolean;
required?: boolean;
selected?: boolean;
checked?: boolean | 'mixed';
pressed?: boolean | 'mixed';
level?: number;
valuemin?: number;
valuemax?: number;
autocomplete?: string;
haspopup?: string;
invalid?: string;
orientation?: string;
children?: SerializedAXNode[];
}
export class Accessibility {
private _client: CDPSession;
constructor(client: CDPSession) {
this._client = client;
}
public async snapshot(
options: { interestingOnly?: boolean; root?: ElementHandle } = {}
): Promise<SerializedAXNode> {
const { interestingOnly = true, root = null } = options;
const { nodes } = await this._client.send('Accessibility.getFullAXTree');
let backendNodeId = null;
if (root) {
const { node } = await this._client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId,
});
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(nodes);
let needle = defaultRoot;
if (backendNodeId) {
needle = defaultRoot.find(
(node) => node.payload.backendDOMNodeId === backendNodeId
);
if (!needle) return null;
}
if (!interestingOnly) return this.serializeTree(needle)[0];
const interestingNodes = new Set<AXNode>();
this.collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle)) return null;
return this.serializeTree(needle, interestingNodes)[0];
}
private serializeTree(
node: AXNode,
whitelistedNodes?: Set<AXNode>
): SerializedAXNode[] {
const children: SerializedAXNode[] = [];
for (const child of node.children)
children.push(...this.serializeTree(child, whitelistedNodes));
if (whitelistedNodes && !whitelistedNodes.has(node)) return children;
const serializedNode = node.serialize();
if (children.length) serializedNode.children = children;
return [serializedNode];
}
private collectInterestingNodes(
collection: Set<AXNode>,
node: AXNode,
insideControl: boolean
): void {
if (node.isInteresting(insideControl)) collection.add(node);
if (node.isLeafNode()) return;
insideControl = insideControl || node.isControl();
for (const child of node.children)
this.collectInterestingNodes(collection, child, insideControl);
}
}
class AXNode {
public payload: Protocol.Accessibility.AXNode;
public children: AXNode[] = [];
private _richlyEditable = false;
private _editable = false;
private _focusable = false;
private _hidden = false;
private _name: string;
private _role: string;
private _cachedHasFocusableChild?: boolean;
constructor(payload: Protocol.Accessibility.AXNode) {
this.payload = payload;
this._name = this.payload.name ? this.payload.name.value : '';
this._role = this.payload.role ? this.payload.role.value : 'Unknown';
for (const property of this.payload.properties || []) {
if (property.name === 'editable') {
this._richlyEditable = property.value.value === 'richtext';
this._editable = true;
}
if (property.name === 'focusable') this._focusable = property.value.value;
if (property.name === 'hidden') this._hidden = property.value.value;
}
}
private _isPlainTextField(): boolean {
if (this._richlyEditable) return false;
if (this._editable) return true;
return (
this._role === 'textbox' ||
this._role === 'ComboBox' ||
this._role === 'searchbox'
);
}
private _isTextOnlyObject(): boolean {
const role = this._role;
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
}
private _hasFocusableChild(): boolean {
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;
}
public find(predicate: (x: AXNode) => boolean): AXNode | null {
if (predicate(this)) return this;
for (const child of this.children) {
const result = child.find(predicate);
if (result) return result;
}
return null;
}
public isLeafNode(): boolean {
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;
}
public isControl(): boolean {
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;
}
}
public isInteresting(insideControl: boolean): boolean {
const role = this._role;
if (role === 'Ignored' || this._hidden) return false;
if (this._focusable || this._richlyEditable) return true;
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl()) return true;
// A non focusable child of a control is not interesting
if (insideControl) return false;
return this.isLeafNode() && !!this._name;
}
public serialize(): SerializedAXNode {
const properties = new Map<string, number | string | boolean>();
for (const property of this.payload.properties || [])
properties.set(property.name.toLowerCase(), property.value.value);
if (this.payload.name) properties.set('name', this.payload.name.value);
if (this.payload.value) properties.set('value', this.payload.value.value);
if (this.payload.description)
properties.set('description', this.payload.description.value);
const node: SerializedAXNode = {
role: this._role,
};
type UserStringProperty =
| 'name'
| 'value'
| 'description'
| 'keyshortcuts'
| 'roledescription'
| 'valuetext';
const userStringProperties: UserStringProperty[] = [
'name',
'value',
'description',
'keyshortcuts',
'roledescription',
'valuetext',
];
const getUserStringPropertyValue = (key: UserStringProperty): string =>
properties.get(key) as string;
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty)) continue;
node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
}
type BooleanProperty =
| 'disabled'
| 'expanded'
| 'focused'
| 'modal'
| 'multiline'
| 'multiselectable'
| 'readonly'
| 'required'
| 'selected';
const booleanProperties: BooleanProperty[] = [
'disabled',
'expanded',
'focused',
'modal',
'multiline',
'multiselectable',
'readonly',
'required',
'selected',
];
const getBooleanPropertyValue = (key: BooleanProperty): boolean =>
properties.get(key) as boolean;
for (const booleanProperty of booleanProperties) {
// WebArea's treat focus differently than other nodes. They report whether their frame has focus,
// not whether focus is specifically on the root node.
if (booleanProperty === 'focused' && this._role === 'WebArea') continue;
const value = getBooleanPropertyValue(booleanProperty);
if (!value) continue;
node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
}
type TristateProperty = 'checked' | 'pressed';
const tristateProperties: TristateProperty[] = ['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 NumbericalProperty = 'level' | 'valuemax' | 'valuemin';
const numericalProperties: NumbericalProperty[] = [
'level',
'valuemax',
'valuemin',
];
const getNumericalPropertyValue = (key: NumbericalProperty): number =>
properties.get(key) as number;
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty)) continue;
node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
}
type TokenProperty =
| 'autocomplete'
| 'haspopup'
| 'invalid'
| 'orientation';
const tokenProperties: TokenProperty[] = [
'autocomplete',
'haspopup',
'invalid',
'orientation',
];
const getTokenPropertyValue = (key: TokenProperty): string =>
properties.get(key) as string;
for (const tokenProperty of tokenProperties) {
const value = getTokenPropertyValue(tokenProperty);
if (!value || value === 'false') continue;
node[tokenProperty] = getTokenPropertyValue(tokenProperty);
}
return node;
}
public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode {
const nodeById = new Map<string, AXNode>();
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;
}
}

Просмотреть файл

@ -0,0 +1,391 @@
/**
* 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.
*/
import { helper, assert } from './helper';
import { Target } from './Target';
import * as EventEmitter from 'events';
import { Events } from './Events';
import { Connection } from './Connection';
import { Page } from './Page';
import { ChildProcess } from 'child_process';
import type { Viewport } from './PuppeteerViewport';
type BrowserCloseCallback = () => Promise<void> | void;
export class Browser extends EventEmitter {
static async create(
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback
): Promise<Browser> {
const browser = new Browser(
connection,
contextIds,
ignoreHTTPSErrors,
defaultViewport,
process,
closeCallback
);
await connection.send('Target.setDiscoverTargets', { discover: true });
return browser;
}
_ignoreHTTPSErrors: boolean;
_defaultViewport?: Viewport;
_process?: ChildProcess;
_connection: Connection;
_closeCallback: BrowserCloseCallback;
_defaultContext: BrowserContext;
_contexts: Map<string, BrowserContext>;
// TODO: once Target is in TypeScript we can type this properly.
_targets: Map<string, Target>;
constructor(
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback
) {
super();
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
this._process = process;
this._connection = connection;
this._closeCallback = closeCallback || function (): void {};
this._defaultContext = new BrowserContext(this._connection, this, null);
this._contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(
contextId,
new BrowserContext(this._connection, this, contextId)
);
this._targets = new Map();
this._connection.on(Events.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)
);
}
process(): ChildProcess | null {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const { browserContextId } = await this._connection.send(
'Target.createBrowserContext'
);
const context = new BrowserContext(
this._connection,
this,
browserContextId
);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
}
/**
* @param {?string} contextId
*/
async _disposeContext(contextId?: string): Promise<void> {
await this._connection.send('Target.disposeBrowserContext', {
browserContextId: contextId || undefined,
});
this._contexts.delete(contextId);
}
async _targetCreated(
event: Protocol.Target.targetCreatedPayload
): Promise<void> {
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
);
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: { targetId: string }): Promise<void> {
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: Protocol.Target.targetInfoChangedPayload): void {
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);
}
}
wsEndpoint(): string {
return this._connection.url();
}
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
async _createPageInContext(contextId?: string): Promise<Page> {
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;
}
targets(): Target[] {
return Array.from(this._targets.values()).filter(
(target) => target._isInitialized
);
}
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: (x: Target) => boolean,
options: { timeout?: number } = {}
): Promise<Target> {
const { timeout = 30000 } = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget) return existingTarget;
let resolve;
const targetPromise = new Promise<Target>((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<Target>(
targetPromise,
'target',
timeout
);
} finally {
this.removeListener(Events.Browser.TargetCreated, check);
this.removeListener(Events.Browser.TargetChanged, check);
}
function check(target: Target): void {
if (predicate(target)) resolve(target);
}
}
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(
this.browserContexts().map((context) => context.pages())
);
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
}
async version(): Promise<string> {
const version = await this._getVersion();
return version.product;
}
async userAgent(): Promise<string> {
const version = await this._getVersion();
return version.userAgent;
}
async close(): Promise<void> {
await this._closeCallback.call(null);
this.disconnect();
}
disconnect(): void {
this._connection.dispose();
}
isConnected(): boolean {
return !this._connection._closed;
}
_getVersion(): Promise<Protocol.Browser.getVersionReturnValue> {
return this._connection.send('Browser.getVersion');
}
}
export class BrowserContext extends EventEmitter {
_connection: Connection;
_browser: Browser;
_id?: string;
constructor(connection: Connection, browser: Browser, contextId?: string) {
super();
this._connection = connection;
this._browser = browser;
this._id = contextId;
}
targets(): Target[] {
return this._browser
.targets()
.filter((target) => target.browserContext() === this);
}
waitForTarget(
predicate: (x: Target) => boolean,
options: { timeout?: number }
): Promise<Target> {
return this._browser.waitForTarget(
(target) => target.browserContext() === this && predicate(target),
options
);
}
async pages(): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
.filter((target) => target.type() === 'page')
.map((target) => target.page())
);
return pages.filter((page) => !!page);
}
isIncognito(): boolean {
return !!this._id;
}
async overridePermissions(
origin: string,
permissions: Protocol.Browser.PermissionType[]
): Promise<void> {
const webPermissionToProtocol = new Map<
string,
Protocol.Browser.PermissionType
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
permissions = permissions.map((permission) => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {
origin,
browserContextId: this._id || undefined,
permissions,
});
}
async clearPermissionOverrides(): Promise<void> {
await this._connection.send('Browser.resetPermissions', {
browserContextId: this._id || undefined,
});
}
newPage(): Promise<Page> {
return this._browser._createPageInContext(this._id);
}
browser(): Browser {
return this._browser;
}
async close(): Promise<void> {
assert(this._id, 'Non-incognito profiles cannot be closed!');
await this._browser._disposeContext(this._id);
}
}

Просмотреть файл

@ -0,0 +1,525 @@
/**
* 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.
*/
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import * as childProcess from 'child_process';
import * as https from 'https';
import * as http from 'http';
import * as extractZip from 'extract-zip';
import * as debug from 'debug';
import * as removeRecursive from 'rimraf';
import * as URL from 'url';
import * as ProxyAgent from 'https-proxy-agent';
import { getProxyForUrl } from 'proxy-from-env';
import { helper, assert } from './helper';
const debugFetcher = debug(`puppeteer:fetcher`);
const downloadURLs = {
chrome: {
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip',
win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip',
},
firefox: {
linux: '%s/firefox-%s.en-US.%s-x86_64.tar.bz2',
mac: '%s/firefox-%s.en-US.%s.dmg',
win32: '%s/firefox-%s.en-US.%s.zip',
win64: '%s/firefox-%s.en-US.%s.zip',
},
} as const;
const browserConfig = {
chrome: {
host: 'https://storage.googleapis.com',
destination: '.local-chromium',
},
firefox: {
host:
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central',
destination: '.local-firefox',
},
} as const;
type Platform = 'linux' | 'mac' | 'win32' | 'win64';
type Product = 'chrome' | 'firefox';
function archiveName(
product: Product,
platform: Platform,
revision: string
): string {
if (product === 'chrome') {
if (platform === 'linux') return 'chrome-linux';
if (platform === 'mac') return 'chrome-mac';
if (platform === 'win32' || platform === 'win64') {
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
}
} else if (product === 'firefox') {
return platform;
}
}
/**
* @param {string} product
* @param {string} platform
* @param {string} host
* @param {string} revision
* @return {string}
*/
function downloadURL(
product: Product,
platform: Platform,
host: string,
revision: string
): string {
const url = util.format(
downloadURLs[product][platform],
host,
revision,
archiveName(product, platform, revision)
);
return url;
}
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: string): Promise<boolean> {
return new Promise((resolve) => {
fs.access(filePath, (err) => resolve(!err));
});
}
export interface BrowserFetcherOptions {
platform?: Platform;
product?: string;
path?: string;
host?: string;
}
interface BrowserFetcherRevisionInfo {
folderPath: string;
executablePath: string;
url: string;
local: boolean;
revision: string;
product: string;
}
/**
*/
export class BrowserFetcher {
private _product: Product;
private _downloadsFolder: string;
private _downloadHost: string;
private _platform: Platform;
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
this._product = (options.product || 'chrome').toLowerCase() as Product;
assert(
this._product === 'chrome' || this._product === 'firefox',
`Unknown product: "${options.product}"`
);
this._downloadsFolder =
options.path ||
path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host;
this.setPlatform(options.platform);
assert(
downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform
);
}
private setPlatform(platformFromOptions?: Platform): void {
if (platformFromOptions) {
this._platform = platformFromOptions;
return;
}
const platform = os.platform();
if (platform === 'darwin') this._platform = 'mac';
else if (platform === 'linux') this._platform = 'linux';
else if (platform === 'win32')
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
else assert(this._platform, 'Unsupported platform: ' + os.platform());
}
platform(): string {
return this._platform;
}
product(): string {
return this._product;
}
host(): string {
return this._downloadHost;
}
canDownload(revision: string): Promise<boolean> {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
return new Promise((resolve) => {
const request = httpRequest(url, 'HEAD', (response) => {
resolve(response.statusCode === 200);
});
request.on('error', (error) => {
console.error(error);
resolve(false);
});
});
}
/**
* @param {string} revision
* @param {?function(number, number):void} progressCallback
* @return {!Promise<!BrowserFetcher.RevisionInfo>}
*/
async download(
revision: string,
progressCallback: (x: number, y: number) => void
): Promise<BrowserFetcherRevisionInfo> {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
const fileName = url.split('/').pop();
const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder)))
await mkdirAsync(this._downloadsFolder);
try {
await downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath);
} finally {
if (await existsAsync(archivePath)) await unlinkAsync(archivePath);
}
const revisionInfo = this.revisionInfo(revision);
if (revisionInfo) await chmodAsync(revisionInfo.executablePath, 0o755);
return revisionInfo;
}
async localRevisions(): Promise<string[]> {
if (!(await existsAsync(this._downloadsFolder))) return [];
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(this._product, fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}
async remove(revision: string): Promise<void> {
const folderPath = this._getFolderPath(revision);
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`
);
await new Promise((fulfill) => removeRecursive(folderPath, fulfill));
}
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
const folderPath = this._getFolderPath(revision);
let executablePath = '';
if (this._product === 'chrome') {
if (this._platform === 'mac')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
else if (this._platform === 'linux')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'chrome'
);
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
'chrome.exe'
);
else throw new Error('Unsupported platform: ' + this._platform);
} else if (this._product === 'firefox') {
if (this._platform === 'mac')
executablePath = path.join(
folderPath,
'Firefox Nightly.app',
'Contents',
'MacOS',
'firefox'
);
else if (this._platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this._platform === 'win32' || this._platform === 'win64')
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else throw new Error('Unsupported platform: ' + this._platform);
} else {
throw new Error('Unsupported product: ' + this._product);
}
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
revision
);
const local = fs.existsSync(folderPath);
debugFetcher({
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
});
return {
revision,
executablePath,
folderPath,
local,
url,
product: this._product,
};
}
/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision: string): string {
return path.join(this._downloadsFolder, this._platform + '-' + revision);
}
}
function parseFolderPath(
product: Product,
folderPath: string
): { product: string; platform: string; revision: string } | null {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2) return null;
const [platform, revision] = splits;
if (!downloadURLs[product][platform]) return null;
return { product, platform, revision };
}
/**
* @param {string} url
* @param {string} destinationPath
* @param {?function(number, number):void} progressCallback
* @return {!Promise}
*/
function downloadFile(
url: string,
destinationPath: string,
progressCallback: (x: number, y: number) => void
): Promise<void> {
debugFetcher(`Downloading binary from ${url}`);
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
const promise = new Promise<void>((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: string): void {
downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
}
}
function install(archivePath: string, folderPath: string): Promise<unknown> {
debugFetcher(`Installing ${archivePath} to ${folderPath}`);
if (archivePath.endsWith('.zip'))
return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2'))
return extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg'))
return mkdirAsync(folderPath).then(() =>
installDMG(archivePath, folderPath)
);
else throw new Error(`Unsupported archive format: ${archivePath}`);
}
/**
* @param {string} tarPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tar = require('tar-fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const bzip = require('unbzip2-stream');
return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath);
tarStream.on('error', reject);
tarStream.on('finish', fulfill);
const readStream = fs.createReadStream(tarPath);
readStream.pipe(bzip()).pipe(tarStream);
});
}
/**
* Install *.app directory from dmg file
*
* @param {string} dmgPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function installDMG(dmgPath: string, folderPath: string): Promise<void> {
let mountPath;
function mountAndCopy(fulfill: () => void, reject: (Error) => void): void {
const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`;
childProcess.exec(mountCommand, (err, stdout) => {
if (err) return reject(err);
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes)
return reject(new Error(`Could not find volume path in ${stdout}`));
mountPath = volumes[0];
readdirAsync(mountPath)
.then((fileNames) => {
const appName = fileNames.filter(
(item) => typeof item === 'string' && item.endsWith('.app')
)[0];
if (!appName)
return reject(new Error(`Cannot find app in ${mountPath}`));
const copyPath = path.join(mountPath, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => {
if (err) reject(err);
else fulfill();
});
})
.catch(reject);
});
}
function unmount(): void {
if (!mountPath) return;
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, (err) => {
if (err) console.error(`Error unmounting dmg: ${err}`);
});
}
return new Promise<void>(mountAndCopy)
.catch((error) => {
console.error(error);
})
.finally(unmount);
}
function httpRequest(
url: string,
method: string,
response: (x: http.IncomingMessage) => void
): http.ClientRequest {
const urlParsed = URL.parse(url);
type Options = Partial<URL.UrlWithStringQuery> & {
method?: string;
agent?: ProxyAgent;
rejectUnauthorized?: boolean;
};
let options: Options = {
...urlParsed,
method,
};
const proxyURL = getProxyForUrl(url);
if (proxyURL) {
if (url.startsWith('http:')) {
const proxy = URL.parse(proxyURL);
options = {
path: options.href,
host: proxy.hostname,
port: proxy.port,
};
} else {
const parsedProxyURL = URL.parse(proxyURL);
const proxyOptions = {
...parsedProxyURL,
secureProxy: parsedProxyURL.protocol === 'https:',
} as ProxyAgent.HttpsProxyAgentOptions;
options.agent = new ProxyAgent(proxyOptions);
options.rejectUnauthorized = false;
}
}
const requestCallback = (res: http.IncomingMessage): void => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
httpRequest(res.headers.location, method, response);
else response(res);
};
const request =
options.protocol === 'https:'
? https.request(options, requestCallback)
: http.request(options, requestCallback);
request.end();
return request;
}

Просмотреть файл

@ -0,0 +1,270 @@
/**
* 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.
*/
import { assert } from './helper';
import { Events } from './Events';
import * as debug from 'debug';
const debugProtocol = debug('puppeteer:protocol');
import type { ConnectionTransport } from './ConnectionTransport';
import * as EventEmitter from 'events';
interface ConnectionCallback {
resolve: Function;
reject: Function;
error: Error;
method: string;
}
export class Connection extends EventEmitter {
_url: string;
_transport: ConnectionTransport;
_delay: number;
_lastId = 0;
_sessions: Map<string, CDPSession> = new Map();
_closed = false;
_callbacks: Map<number, ConnectionCallback> = new Map();
constructor(url: string, transport: ConnectionTransport, delay = 0) {
super();
this._url = url;
this._delay = delay;
this._transport = transport;
this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
}
static fromSession(session: CDPSession): Connection {
return session._connection;
}
/**
* @param {string} sessionId
* @return {?CDPSession}
*/
session(sessionId: string): CDPSession | null {
return this._sessions.get(sessionId) || null;
}
url(): string {
return this._url;
}
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
const id = this._rawSend({ method, params });
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
_rawSend(message: {}): number {
const id = ++this._lastId;
message = JSON.stringify(Object.assign({}, message, { id }));
debugProtocol('SEND ► ' + message);
this._transport.send(message);
return id;
}
async _onMessage(message: string): Promise<void> {
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(): void {
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(): void {
this._onClose();
this._transport.close();
}
/**
* @param {Protocol.Target.TargetInfo} targetInfo
* @return {!Promise<!CDPSession>}
*/
async createSession(
targetInfo: Protocol.Target.TargetInfo
): Promise<CDPSession> {
const { sessionId } = await this.send('Target.attachToTarget', {
targetId: targetInfo.targetId,
flatten: true,
});
return this._sessions.get(sessionId);
}
}
interface CDPSessionOnMessageObject {
id?: number;
method: string;
params: {};
error: { message: string; data: any };
result?: any;
}
export class CDPSession extends EventEmitter {
_connection: Connection;
_sessionId: string;
_targetType: string;
_callbacks: Map<number, ConnectionCallback> = new Map();
constructor(connection: Connection, targetType: string, sessionId: string) {
super();
this._connection = connection;
this._targetType = targetType;
this._sessionId = sessionId;
}
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (!this._connection)
return Promise.reject(
new Error(
`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`
)
);
const id = this._connection._rawSend({
sessionId: this._sessionId,
method,
/* TODO(jacktfranklin@): once this Firefox bug is solved
* we no longer need the `|| {}` check
* https://bugzilla.mozilla.org/show_bug.cgi?id=1631570
*/
params: params || {},
});
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new Error(), method });
});
}
_onMessage(object: CDPSessionOnMessageObject): void {
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(): Promise<void> {
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(): void {
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: Error,
method: string,
object: { error: { message: string; data: any } }
): Error {
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: Error, message: string): Error {
error.message = message;
return error;
}

Просмотреть файл

@ -1,5 +1,5 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,8 +14,9 @@
* limitations under the License.
*/
const TestRunner = require('./TestRunner');
const Reporter = require('./Reporter');
const Matchers = require('./Matchers');
module.exports = { TestRunner, Reporter, Matchers };
export interface ConnectionTransport {
send(string);
close();
onmessage?: (message: string) => void;
onclose?: () => void;
}

Просмотреть файл

@ -0,0 +1,58 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JSHandle } from './JSHandle';
interface ConsoleMessageLocation {
url?: string;
lineNumber?: number;
columnNumber?: number;
}
export class ConsoleMessage {
private _type: string;
private _text: string;
private _args: JSHandle[];
private _location: ConsoleMessageLocation;
constructor(
type: string,
text: string,
args: JSHandle[],
location: ConsoleMessageLocation = {}
) {
this._type = type;
this._text = text;
this._args = args;
this._location = location;
}
type(): string {
return this._type;
}
text(): string {
return this._text;
}
args(): JSHandle[] {
return this._args;
}
location(): ConsoleMessageLocation {
return this._location;
}
}

Просмотреть файл

@ -14,78 +14,71 @@
* limitations under the License.
*/
const {helper, debugError, assert} = require('./helper');
import { helper, debugError, assert, PuppeteerEventListener } from './helper';
import { CDPSession } from './Connection';
const {EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
import { EVALUATION_SCRIPT_URL } from './ExecutionContext';
/**
* @typedef {Object} CoverageEntry
* @property {string} url
* @property {string} text
* @property {!Array<!{start: number, end: number}>} ranges
*/
interface CoverageEntry {
url: string;
text: string;
ranges: Array<{ start: number; end: number }>;
}
class Coverage {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
export class Coverage {
_jsCoverage: JSCoverage;
_cssCoverage: CSSCoverage;
constructor(client: CDPSession) {
this._jsCoverage = new JSCoverage(client);
this._cssCoverage = new CSSCoverage(client);
}
/**
* @param {!{resetOnNavigation?: boolean, reportAnonymousScripts?: boolean}} options
*/
async startJSCoverage(options) {
async startJSCoverage(options: {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
}): Promise<void> {
return await this._jsCoverage.start(options);
}
/**
* @return {!Promise<!Array<!CoverageEntry>>}
*/
async stopJSCoverage() {
async stopJSCoverage(): Promise<CoverageEntry[]> {
return await this._jsCoverage.stop();
}
/**
* @param {{resetOnNavigation?: boolean}=} options
*/
async startCSSCoverage(options) {
async startCSSCoverage(options: {
resetOnNavigation?: boolean;
}): Promise<void> {
return await this._cssCoverage.start(options);
}
/**
* @return {!Promise<!Array<!CoverageEntry>>}
*/
async stopCSSCoverage() {
async stopCSSCoverage(): Promise<CoverageEntry[]> {
return await this._cssCoverage.stop();
}
}
module.exports = {Coverage};
class JSCoverage {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
_client: CDPSession;
_enabled = false;
_scriptURLs = new Map<string, string>();
_scriptSources = new Map<string, string>();
_eventListeners: PuppeteerEventListener[] = [];
_resetOnNavigation = false;
_reportAnonymousScripts = false;
constructor(client: CDPSession) {
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 = {}) {
async start(
options: {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
} = {}
): Promise<void> {
assert(!this._enabled, 'JSCoverage is already enabled');
const {
resetOnNavigation = true,
reportAnonymousScripts = false
reportAnonymousScripts = false,
} = options;
this._resetOnNavigation = resetOnNavigation;
this._reportAnonymousScripts = reportAnonymousScripts;
@ -93,102 +86,120 @@ class JSCoverage {
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)),
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('Profiler.startPreciseCoverage', {
callCount: false,
detailed: true,
}),
this._client.send('Debugger.enable'),
this._client.send('Debugger.setSkipAllPauses', {skip: true})
this._client.send('Debugger.setSkipAllPauses', { skip: true }),
]);
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
return;
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation) return;
this._scriptURLs.clear();
this._scriptSources.clear();
}
/**
* @param {!Protocol.Debugger.scriptParsedPayload} event
*/
async _onScriptParsed(event) {
async _onScriptParsed(
event: Protocol.Debugger.scriptParsedPayload
): Promise<void> {
// Ignore puppeteer-injected scripts
if (event.url === EVALUATION_SCRIPT_URL)
return;
if (event.url === EVALUATION_SCRIPT_URL) return;
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts)
return;
if (!event.url && !this._reportAnonymousScripts) return;
try {
const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId});
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) {
} catch (error) {
// This might happen if the page has already navigated away.
debugError(e);
debugError(error);
}
}
/**
* @return {!Promise<!Array<!CoverageEntry>>}
*/
async stop() {
async stop(): Promise<CoverageEntry[]> {
assert(this._enabled, 'JSCoverage is not enabled');
this._enabled = false;
const [profileResponse] = await Promise.all([
const result = await Promise.all<
Protocol.Profiler.takePreciseCoverageReturnValue,
Protocol.Profiler.stopPreciseCoverageReturnValue,
Protocol.Profiler.disableReturnValue,
Protocol.Debugger.disableReturnValue
>([
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 = [];
const profileResponse = result[0];
for (const entry of profileResponse.result) {
let url = this._scriptURLs.get(entry.scriptId);
if (!url && this._reportAnonymousScripts)
url = 'debugger://VM' + entry.scriptId;
const text = this._scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined)
continue;
if (text === undefined || url === undefined) continue;
const flattenRanges = [];
for (const func of entry.functions)
flattenRanges.push(...func.ranges);
for (const func of entry.functions) flattenRanges.push(...func.ranges);
const ranges = convertToDisjointRanges(flattenRanges);
coverage.push({url, ranges, text});
coverage.push({ url, ranges, text });
}
return coverage;
}
}
class CSSCoverage {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
_client: CDPSession;
_enabled = false;
_stylesheetURLs = new Map<string, string>();
_stylesheetSources = new Map<string, string>();
_eventListeners: PuppeteerEventListener[] = [];
_resetOnNavigation = false;
_reportAnonymousScripts = false;
constructor(client: CDPSession) {
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 = {}) {
async start(options: { resetOnNavigation?: boolean } = {}): Promise<void> {
assert(!this._enabled, 'CSSCoverage is already enabled');
const {resetOnNavigation = true} = options;
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)),
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'),
@ -197,38 +208,36 @@ class CSSCoverage {
]);
}
_onExecutionContextsCleared() {
if (!this._resetOnNavigation)
return;
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation) return;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
}
/**
* @param {!Protocol.CSS.styleSheetAddedPayload} event
*/
async _onStyleSheet(event) {
async _onStyleSheet(
event: Protocol.CSS.styleSheetAddedPayload
): Promise<void> {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL)
return;
if (!header.sourceURL) return;
try {
const response = await this._client.send('CSS.getStyleSheetText', {styleSheetId: header.styleSheetId});
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) {
} catch (error) {
// This might happen if the page has already navigated away.
debugError(e);
debugError(error);
}
}
/**
* @return {!Promise<!Array<!CoverageEntry>>}
*/
async stop() {
async stop(): Promise<CoverageEntry[]> {
assert(this._enabled, 'CSSCoverage is not enabled');
this._enabled = false;
const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking');
const ruleTrackingResponse = await this._client.send(
'CSS.stopRuleUsageTracking'
);
await Promise.all([
this._client.send('CSS.disable'),
this._client.send('DOM.disable'),
@ -254,19 +263,19 @@ class CSSCoverage {
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});
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) {
function convertToDisjointRanges(
nestedRanges: Array<{ startOffset: number; endOffset: number; count: number }>
): Array<{ start: number; end: number }> {
const points = [];
for (const range of nestedRanges) {
points.push({ offset: range.startOffset, type: 0, range });
@ -275,16 +284,13 @@ function convertToDisjointRanges(nestedRanges) {
// 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;
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;
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;
if (a.type === 0) return bLength - aLength;
// For two "end" points, the one with shorter range goes first.
return aLength - bLength;
});
@ -294,20 +300,20 @@ function convertToDisjointRanges(nestedRanges) {
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) {
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});
else results.push({ start: lastOffset, end: point.offset });
}
lastOffset = point.offset;
if (point.type === 0)
hitCountStack.push(point.range.count);
else
hitCountStack.pop();
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);
return results.filter((range) => range.end - range.start > 1);
}

Просмотреть файл

@ -0,0 +1,753 @@
/**
* 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.
*/
import * as fs from 'fs';
import { helper, assert } from './helper';
import { LifecycleWatcher, PuppeteerLifeCycleEvent } from './LifecycleWatcher';
import { TimeoutError } from './Errors';
import { JSHandle, ElementHandle } from './JSHandle';
import { ExecutionContext } from './ExecutionContext';
import { TimeoutSettings } from './TimeoutSettings';
import { MouseButtonInput } from './Input';
import { FrameManager, Frame } from './FrameManager';
import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler';
// This predicateQueryHandler is declared here so that TypeScript knows about it
// when it is used in the predicate function below.
declare const predicateQueryHandler: QueryHandler;
const readFileAsync = helper.promisify(fs.readFile);
export interface WaitForSelectorOptions {
visible?: boolean;
hidden?: boolean;
timeout?: number;
}
export class DOMWorld {
_frameManager: FrameManager;
_frame: Frame;
_timeoutSettings: TimeoutSettings;
_documentPromise?: Promise<ElementHandle> = null;
_contextPromise?: Promise<ExecutionContext> = null;
_contextResolveCallback?: (x?: ExecutionContext) => void = null;
_detached = false;
_waitTasks = new Set<WaitTask>();
constructor(
frameManager: FrameManager,
frame: Frame,
timeoutSettings: TimeoutSettings
) {
this._frameManager = frameManager;
this._frame = frame;
this._timeoutSettings = timeoutSettings;
this._setContext(null);
}
frame(): Frame {
return this._frame;
}
/**
* @param {?ExecutionContext} context
*/
_setContext(context?: ExecutionContext): void {
if (context) {
this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null;
for (const waitTask of this._waitTasks) waitTask.rerun();
} else {
this._documentPromise = null;
this._contextPromise = new Promise((fulfill) => {
this._contextResolveCallback = fulfill;
});
}
}
_hasContext(): boolean {
return !this._contextResolveCallback;
}
_detach(): void {
this._detached = true;
for (const waitTask of this._waitTasks)
waitTask.terminate(
new Error('waitForFunction failed: frame got detached.')
);
}
/**
* @return {!Promise<!ExecutionContext>}
*/
executionContext(): Promise<ExecutionContext> {
if (this._detached)
throw new Error(
`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`
);
return this._contextPromise;
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const context = await this.executionContext();
return context.evaluate<ReturnType>(pageFunction, ...args);
}
/**
* @param {string} selector
* @return {!Promise<?ElementHandle>}
*/
async $(selector: string): Promise<ElementHandle | null> {
const document = await this._document();
const value = await document.$(selector);
return value;
}
async _document(): Promise<ElementHandle> {
if (this._documentPromise) return this._documentPromise;
this._documentPromise = this.executionContext().then(async (context) => {
const document = await context.evaluateHandle('document');
return document.asElement();
});
return this._documentPromise;
}
async $x(expression: string): Promise<ElementHandle[]> {
const document = await this._document();
const value = await document.$x(expression);
return value;
}
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const document = await this._document();
return document.$eval<ReturnType>(selector, pageFunction, ...args);
}
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const document = await this._document();
const value = await document.$$eval<ReturnType>(
selector,
pageFunction,
...args
);
return value;
}
/**
* @param {string} selector
* @return {!Promise<!Array<!ElementHandle>>}
*/
async $$(selector: string): Promise<ElementHandle[]> {
const document = await this._document();
const value = await document.$$(selector);
return value;
}
async content(): Promise<string> {
return await this.evaluate(() => {
let retVal = '';
if (document.doctype)
retVal = new XMLSerializer().serializeToString(document.doctype);
if (document.documentElement)
retVal += document.documentElement.outerHTML;
return retVal;
});
}
async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<void> {
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<!ElementHandle>}
*/
async addScriptTag(options: {
url?: string;
path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
const { url = null, path = null, content = null, type = '' } = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (
await context.evaluateHandle(addScriptUrl, url, type)
).asElement();
} catch (error) {
throw new Error(`Loading script from ${url} failed`);
}
}
if (path !== null) {
let contents = await readFileAsync(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
const context = await this.executionContext();
return (
await context.evaluateHandle(addScriptContent, contents, type)
).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (
await context.evaluateHandle(addScriptContent, content, type)
).asElement();
}
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addScriptUrl(
url: string,
type: string
): Promise<HTMLElement> {
const script = document.createElement('script');
script.src = url;
if (type) script.type = type;
const promise = new Promise((res, rej) => {
script.onload = res;
script.onerror = rej;
});
document.head.appendChild(script);
await promise;
return script;
}
function addScriptContent(
content: string,
type = 'text/javascript'
): HTMLElement {
const script = document.createElement('script');
script.type = type;
script.text = content;
let error = null;
script.onerror = (e) => (error = e);
document.head.appendChild(script);
if (error) throw error;
return script;
}
}
async addStyleTag(options: {
url?: string;
path?: string;
content?: string;
}): Promise<ElementHandle> {
const { url = null, path = null, content = null } = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (await context.evaluateHandle(addStyleUrl, url)).asElement();
} catch (error) {
throw new Error(`Loading style from ${url} failed`);
}
}
if (path !== null) {
let contents = await readFileAsync(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
const context = await this.executionContext();
return (
await context.evaluateHandle(addStyleContent, contents)
).asElement();
}
if (content !== null) {
const context = await this.executionContext();
return (
await context.evaluateHandle(addStyleContent, content)
).asElement();
}
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addStyleUrl(url: string): Promise<HTMLElement> {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
const promise = new Promise((res, rej) => {
link.onload = res;
link.onerror = rej;
});
document.head.appendChild(link);
await promise;
return link;
}
async function addStyleContent(content: string): Promise<HTMLElement> {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(content));
const promise = new Promise((res, rej) => {
style.onload = res;
style.onerror = rej;
});
document.head.appendChild(style);
await promise;
return style;
}
}
async click(
selector: string,
options: { delay?: number; button?: MouseButtonInput; clickCount?: number }
): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.click(options);
await handle.dispose();
}
async focus(selector: string): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.focus();
await handle.dispose();
}
async hover(selector: string): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.hover();
await handle.dispose();
}
async select(selector: string, ...values: string[]): Promise<string[]> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const result = await handle.select(...values);
await handle.dispose();
return result;
}
async tap(selector: string): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.tap();
await handle.dispose();
}
async type(
selector: string,
text: string,
options?: { delay: number }
): Promise<void> {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.type(text, options);
await handle.dispose();
}
waitForSelector(
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
return this._waitForSelectorOrXPath(selector, false, options);
}
waitForXPath(
xpath: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
return this._waitForSelectorOrXPath(xpath, true, options);
}
waitForFunction(
pageFunction: Function | string,
options: { polling?: string | number; timeout?: number } = {},
...args: unknown[]
): Promise<JSHandle> {
const {
polling = 'raf',
timeout = this._timeoutSettings.timeout(),
} = options;
return new WaitTask(
this,
pageFunction,
undefined,
'function',
polling,
timeout,
...args
).promise;
}
async title(): Promise<string> {
return this.evaluate(() => document.title);
}
private async _waitForSelectorOrXPath(
selectorOrXPath: string,
isXPath: boolean,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle | null> {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this._timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${
waitForHidden ? ' to be hidden' : ''
}`;
const {
updatedSelector,
queryHandler,
} = getQueryHandlerAndSelector(selectorOrXPath, (element, selector) =>
document.querySelector(selector)
);
const waitTask = new WaitTask(
this,
predicate,
queryHandler,
title,
polling,
timeout,
updatedSelector,
isXPath,
waitForVisible,
waitForHidden
);
const handle = await waitTask.promise;
if (!handle.asElement()) {
await handle.dispose();
return null;
}
return handle.asElement();
/**
* @param {string} selectorOrXPath
* @param {boolean} isXPath
* @param {boolean} waitForVisible
* @param {boolean} waitForHidden
* @return {?Node|boolean}
*/
function predicate(
selectorOrXPath: string,
isXPath: boolean,
waitForVisible: boolean,
waitForHidden: boolean
): Node | null | boolean {
const node = isXPath
? document.evaluate(
selectorOrXPath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue
: predicateQueryHandler
? (predicateQueryHandler(document, selectorOrXPath) as Element)
: document.querySelector(selectorOrXPath);
if (!node) return waitForHidden;
if (!waitForVisible && !waitForHidden) return node;
const element =
node.nodeType === Node.TEXT_NODE
? node.parentElement
: (node as Element);
const style = window.getComputedStyle(element);
const isVisible =
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success =
waitForVisible === isVisible || waitForHidden === !isVisible;
return success ? node : null;
function hasVisibleBoundingBox(): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
}
}
class WaitTask {
_domWorld: DOMWorld;
_polling: string | number;
_timeout: number;
_predicateBody: string;
_args: unknown[];
_runCount = 0;
promise: Promise<JSHandle>;
_resolve: (x: JSHandle) => void;
_reject: (x: Error) => void;
_timeoutTimer?: NodeJS.Timeout;
_terminated = false;
constructor(
domWorld: DOMWorld,
predicateBody: Function | string,
predicateQueryHandlerBody: Function | string | undefined,
title: string,
polling: string | number,
timeout: number,
...args: unknown[]
) {
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);
function getPredicateBody(
predicateBody: Function | string,
predicateQueryHandlerBody: Function | string
) {
if (helper.isString(predicateBody)) return `return (${predicateBody});`;
if (predicateQueryHandlerBody) {
return `
return (function wrapper(args) {
const predicateQueryHandler = ${predicateQueryHandlerBody};
return (${predicateBody})(...args);
})(args);`;
}
return `return (${predicateBody})(...args);`;
}
this._domWorld = domWorld;
this._polling = polling;
this._timeout = timeout;
this._predicateBody = getPredicateBody(
predicateBody,
predicateQueryHandlerBody
);
this._args = args;
this._runCount = 0;
domWorld._waitTasks.add(this);
this.promise = new Promise<JSHandle>((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();
}
terminate(error: Error): void {
this._terminated = true;
this._reject(error);
this._cleanup();
}
async rerun(): Promise<void> {
const runCount = ++this._runCount;
/** @type {?JSHandle} */
let success = null;
let error = null;
try {
success = await (await this._domWorld.executionContext()).evaluateHandle(
waitForPredicatePageFunction,
this._predicateBody,
this._polling,
this._timeout,
...this._args
);
} catch (error_) {
error = error_;
}
if (this._terminated || runCount !== this._runCount) {
if (success) await success.dispose();
return;
}
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if (
!error &&
(await this._domWorld.evaluate((s) => !s, success).catch(() => true))
) {
await success.dispose();
return;
}
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if (error && error.message.includes('Execution context was destroyed'))
return;
// We could have tried to evaluate in a context which was already
// destroyed.
if (
error &&
error.message.includes('Cannot find context with specified id')
)
return;
if (error) this._reject(error);
else this._resolve(success);
this._cleanup();
}
_cleanup(): void {
clearTimeout(this._timeoutTimer);
this._domWorld._waitTasks.delete(this);
}
}
async function waitForPredicatePageFunction(
predicateBody: string,
polling: string,
timeout: number,
...args: unknown[]
): Promise<unknown> {
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(): Promise<unknown> {
const success = predicate(...args);
if (success) return Promise.resolve(success);
let fulfill;
const result = new Promise((x) => (fulfill = x));
const observer = new MutationObserver(() => {
if (timedOut) {
observer.disconnect();
fulfill();
}
const success = predicate(...args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
observer.observe(document, {
childList: true,
subtree: true,
attributes: true,
});
return result;
}
function pollRaf(): Promise<unknown> {
let fulfill;
const result = new Promise((x) => (fulfill = x));
onRaf();
return result;
function onRaf(): void {
if (timedOut) {
fulfill();
return;
}
const success = predicate(...args);
if (success) fulfill(success);
else requestAnimationFrame(onRaf);
}
}
function pollInterval(pollInterval: number): Promise<unknown> {
let fulfill;
const result = new Promise((x) => (fulfill = x));
onTimeout();
return result;
function onTimeout(): void {
if (timedOut) {
fulfill();
return;
}
const success = predicate(...args);
if (success) fulfill(success);
else setTimeout(onTimeout, pollInterval);
}
}
}

Просмотреть файл

@ -0,0 +1,964 @@
/**
* 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.
*/
interface Device {
name: string;
userAgent: string;
viewport: {
width: number;
height: number;
deviceScaleFactor: number;
isMobile: boolean;
hasTouch: boolean;
isLandscape: boolean;
};
}
const devices: Device[] = [
{
name: 'Blackberry PlayBook',
userAgent:
'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
viewport: {
width: 600,
height: 1024,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Blackberry PlayBook landscape',
userAgent:
'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
viewport: {
width: 1024,
height: 600,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'BlackBerry Z30',
userAgent:
'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'BlackBerry Z30 landscape',
userAgent:
'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy Note 3',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy Note 3 landscape',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy Note II',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy Note II landscape',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy S III',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy S III landscape',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Galaxy S5',
userAgent:
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Galaxy S5 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad landscape',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad Mini',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad Mini landscape',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPad Pro',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1024,
height: 1366,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPad Pro landscape',
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewport: {
width: 1366,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 4',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
viewport: {
width: 320,
height: 480,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 4 landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
viewport: {
width: 480,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 5',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 320,
height: 568,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 5 landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 568,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 6',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 6 landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 6 Plus',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 6 Plus landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 7',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 7 landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 7 Plus',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 7 Plus landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 8',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 8 landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 667,
height: 375,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone 8 Plus',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 414,
height: 736,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone 8 Plus landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 736,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone SE',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 320,
height: 568,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone SE landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewport: {
width: 568,
height: 320,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone X',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 375,
height: 812,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone X landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
viewport: {
width: 812,
height: 375,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'iPhone XR',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
viewport: {
width: 414,
height: 896,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'iPhone XR landscape',
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
viewport: {
width: 896,
height: 414,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'JioPhone 2',
userAgent:
'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
viewport: {
width: 240,
height: 320,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'JioPhone 2 landscape',
userAgent:
'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
viewport: {
width: 320,
height: 240,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Kindle Fire HDX',
userAgent:
'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
viewport: {
width: 800,
height: 1280,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Kindle Fire HDX landscape',
userAgent:
'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
viewport: {
width: 1280,
height: 800,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'LG Optimus L70',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 384,
height: 640,
deviceScaleFactor: 1.25,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'LG Optimus L70 landscape',
userAgent:
'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 384,
deviceScaleFactor: 1.25,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Microsoft Lumia 550',
userAgent:
'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Microsoft Lumia 950',
userAgent:
'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 4,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Microsoft Lumia 950 landscape',
userAgent:
'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 4,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 10',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 800,
height: 1280,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 10 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 1280,
height: 800,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 4',
userAgent:
'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 384,
height: 640,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 4 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 384,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 5',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 360,
height: 640,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 5 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 640,
height: 360,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 5X',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 5X landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 6',
userAgent:
'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 6 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 6P',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 412,
height: 732,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 6P landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 732,
height: 412,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nexus 7',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 600,
height: 960,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nexus 7 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
viewport: {
width: 960,
height: 600,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nokia Lumia 520',
userAgent:
'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
viewport: {
width: 320,
height: 533,
deviceScaleFactor: 1.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nokia Lumia 520 landscape',
userAgent:
'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
viewport: {
width: 533,
height: 320,
deviceScaleFactor: 1.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Nokia N9',
userAgent:
'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
viewport: {
width: 480,
height: 854,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Nokia N9 landscape',
userAgent:
'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
viewport: {
width: 854,
height: 480,
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Pixel 2',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 411,
height: 731,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Pixel 2 landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 731,
height: 411,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
{
name: 'Pixel 2 XL',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 411,
height: 823,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
},
{
name: 'Pixel 2 XL landscape',
userAgent:
'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
viewport: {
width: 823,
height: 411,
deviceScaleFactor: 3.5,
isMobile: true,
hasTouch: true,
isLandscape: true,
},
},
];
export type DevicesMap = {
[name: string]: Device;
};
const devicesMap: DevicesMap = {};
for (const device of devices) devicesMap[device.name] = device;
export { devicesMap };

Просмотреть файл

@ -14,70 +14,66 @@
* limitations under the License.
*/
const {assert} = require('./helper');
import { assert } from './helper';
import { CDPSession } from './Connection';
class Dialog {
/**
* @param {!Puppeteer.CDPSession} client
* @param {string} type
* @param {string} message
* @param {(string|undefined)} defaultValue
*/
constructor(client, type, message, defaultValue = '') {
/* TODO(jacktfranklin): protocol.d.ts defines this
* so let's ditch this and avoid the duplication
*/
export enum DialogType {
Alert = 'alert',
BeforeUnload = 'beforeunload',
Confirm = 'confirm',
Prompt = 'prompt',
}
export class Dialog {
static Type = DialogType;
private _client: CDPSession;
private _type: DialogType;
private _message: string;
private _defaultValue: string;
private _handled = false;
constructor(
client: CDPSession,
type: DialogType,
message: string,
defaultValue = ''
) {
this._client = client;
this._type = type;
this._message = message;
this._handled = false;
this._defaultValue = defaultValue;
}
/**
* @return {string}
*/
type() {
type(): DialogType {
return this._type;
}
/**
* @return {string}
*/
message() {
message(): string {
return this._message;
}
/**
* @return {string}
*/
defaultValue() {
defaultValue(): string {
return this._defaultValue;
}
/**
* @param {string=} promptText
*/
async accept(promptText) {
async accept(promptText?: string): Promise<void> {
assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: true,
promptText: promptText
promptText: promptText,
});
}
async dismiss() {
async dismiss(): Promise<void> {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
accept: false
accept: false,
});
}
}
Dialog.Type = {
Alert: 'alert',
BeforeUnload: 'beforeunload',
Confirm: 'confirm',
Prompt: 'prompt'
};
module.exports = {Dialog};

Просмотреть файл

@ -13,42 +13,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CDPSession } from './Connection';
import type { Viewport } from './PuppeteerViewport';
class EmulationManager {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
export class EmulationManager {
_client: CDPSession;
_emulatingMobile = false;
_hasTouch = false;
constructor(client: CDPSession) {
this._client = client;
this._emulatingMobile = false;
this._hasTouch = false;
}
/**
* @param {!Puppeteer.Viewport} viewport
* @return {Promise<boolean>}
*/
async emulateViewport(viewport) {
async emulateViewport(viewport: Viewport): Promise<boolean> {
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 screenOrientation: Protocol.Emulation.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.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation,
}),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
enabled: hasTouch,
}),
]);
const reloadNeeded = this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
const reloadNeeded =
this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
this._emulatingMobile = mobile;
this._hasTouch = hasTouch;
return reloadNeeded;
}
}
module.exports = {EmulationManager};

Просмотреть файл

@ -15,15 +15,17 @@
*/
class CustomError extends Error {
constructor(message) {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class TimeoutError extends CustomError {}
export class TimeoutError extends CustomError {}
module.exports = {
export type PuppeteerErrors = Record<string, typeof CustomError>;
export const puppeteerErrors: PuppeteerErrors = {
TimeoutError,
};

Просмотреть файл

@ -14,7 +14,7 @@
* limitations under the License.
*/
const Events = {
export const Events = {
Page: {
Close: 'close',
Console: 'console',
@ -42,7 +42,7 @@ const Events = {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',
Disconnected: 'disconnected'
Disconnected: 'disconnected',
},
BrowserContext: {
@ -63,9 +63,15 @@ const Events = {
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'),
FrameNavigatedWithinDocument: Symbol(
'Events.FrameManager.FrameNavigatedWithinDocument'
),
ExecutionContextCreated: Symbol(
'Events.FrameManager.ExecutionContextCreated'
),
ExecutionContextDestroyed: Symbol(
'Events.FrameManager.ExecutionContextDestroyed'
),
},
Connection: {
@ -75,6 +81,4 @@ const Events = {
CDPSession: {
Disconnected: Symbol('Events.CDPSession.Disconnected'),
},
};
module.exports = { Events };
} as const;

Просмотреть файл

@ -0,0 +1,233 @@
/**
* 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.
*/
import { helper, assert } from './helper';
import { createJSHandle, JSHandle, ElementHandle } from './JSHandle';
import { CDPSession } from './Connection';
import { DOMWorld } from './DOMWorld';
import { Frame } from './FrameManager';
export const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export class ExecutionContext {
_client: CDPSession;
_world: DOMWorld;
_contextId: number;
constructor(
client: CDPSession,
contextPayload: Protocol.Runtime.ExecutionContextDescription,
world: DOMWorld
) {
this._client = client;
this._world = world;
this._contextId = contextPayload.id;
}
frame(): Frame | null {
return this._world ? this._world.frame() : null;
}
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this._evaluateInternal<ReturnType>(
true,
pageFunction,
...args
);
}
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return this._evaluateInternal<JSHandle>(false, pageFunction, ...args);
}
private async _evaluateInternal<ReturnType>(
returnByValue,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (helper.isString(pageFunction)) {
const contextId = this._contextId;
const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression
: expression + '\n' + suffix;
const { exceptionDetails, result: remoteObject } = await this._client
.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue,
awaitPromise: true,
userGesture: true,
})
.catch(rewriteError);
if (exceptionDetails)
throw new Error(
'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)
);
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
}
if (typeof pageFunction !== 'function')
throw new Error(
`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`
);
let functionText = pageFunction.toString();
try {
new Function('(' + functionText + ')');
} catch (error) {
// This means we might have a function shorthand. Try another
// time prefixing 'function '.
if (functionText.startsWith('async '))
functionText =
'async function ' + functionText.substring('async '.length);
else functionText = 'function ' + functionText;
try {
new Function('(' + functionText + ')');
} catch (error) {
// We tried hard to serialize, but there's a weird beast here.
throw new Error('Passed function is not well-serializable!');
}
}
let callFunctionOnPromise;
try {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n',
executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)),
returnByValue,
awaitPromise: true,
userGesture: true,
});
} catch (error) {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
)
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
const {
exceptionDetails,
result: remoteObject,
} = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails)
throw new Error(
'Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)
);
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
/**
* @param {*} arg
* @return {*}
* @this {ExecutionContext}
*/
function convertArgument(this: ExecutionContext, arg: unknown): unknown {
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 };
}
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Object reference chain is too long'))
return { result: { type: 'undefined' } };
if (error.message.includes("Object couldn't be returned by value"))
return { result: { type: 'undefined' } };
if (
error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed')
)
throw new Error(
'Execution context was destroyed, most likely because of a navigation.'
);
throw error;
}
}
async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> {
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);
}
async _adoptBackendNodeId(
backendNodeId: Protocol.DOM.BackendNodeId
): Promise<ElementHandle> {
const { object } = await this._client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle;
}
async _adoptElementHandle(
elementHandle: ElementHandle
): Promise<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,
});
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId);
}
}

Просмотреть файл

@ -0,0 +1,53 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ElementHandle } from './JSHandle';
import { assert } from './helper';
export class FileChooser {
private _element: ElementHandle;
private _multiple: boolean;
private _handled = false;
constructor(
element: ElementHandle,
event: Protocol.Page.fileChooserOpenedPayload
) {
this._element = element;
this._multiple = event.mode !== 'selectSingle';
}
isMultiple(): boolean {
return this._multiple;
}
async accept(filePaths: string[]): Promise<void> {
assert(
!this._handled,
'Cannot accept FileChooser which is already handled!'
);
this._handled = true;
await this._element.uploadFile(...filePaths);
}
async cancel(): Promise<void> {
assert(
!this._handled,
'Cannot cancel FileChooser which is already handled!'
);
this._handled = true;
}
}

Просмотреть файл

@ -0,0 +1,668 @@
/**
* 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.
*/
import * as EventEmitter from 'events';
import { helper, assert, debugError } from './helper';
import { Events } from './Events';
import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext';
import { LifecycleWatcher, PuppeteerLifeCycleEvent } from './LifecycleWatcher';
import { DOMWorld, WaitForSelectorOptions } from './DOMWorld';
import { NetworkManager } from './NetworkManager';
import { TimeoutSettings } from './TimeoutSettings';
import { CDPSession } from './Connection';
import { JSHandle, ElementHandle } from './JSHandle';
import { MouseButtonInput } from './Input';
import { Page } from './Page';
import { Response } from './Response';
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
export class FrameManager extends EventEmitter {
_client: CDPSession;
_page: Page;
_networkManager: NetworkManager;
_timeoutSettings: TimeoutSettings;
_frames = new Map<string, Frame>();
_contextIdToContext = new Map<number, ExecutionContext>();
_isolatedWorlds = new Set<string>();
_mainFrame: Frame;
constructor(
client: CDPSession,
page: Page,
ignoreHTTPSErrors: boolean,
timeoutSettings: TimeoutSettings
) {
super();
this._client = client;
this._page = page;
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this._client.on('Page.frameAttached', (event) =>
this._onFrameAttached(event.frameId, event.parentFrameId)
);
this._client.on('Page.frameNavigated', (event) =>
this._onFrameNavigated(event.frame)
);
this._client.on('Page.navigatedWithinDocument', (event) =>
this._onFrameNavigatedWithinDocument(event.frameId, event.url)
);
this._client.on('Page.frameDetached', (event) =>
this._onFrameDetached(event.frameId)
);
this._client.on('Page.frameStoppedLoading', (event) =>
this._onFrameStoppedLoading(event.frameId)
);
this._client.on('Runtime.executionContextCreated', (event) =>
this._onExecutionContextCreated(event.context)
);
this._client.on('Runtime.executionContextDestroyed', (event) =>
this._onExecutionContextDestroyed(event.executionContextId)
);
this._client.on('Runtime.executionContextsCleared', () =>
this._onExecutionContextsCleared()
);
this._client.on('Page.lifecycleEvent', (event) =>
this._onLifecycleEvent(event)
);
}
async initialize(): Promise<void> {
const result = await Promise.all<
Protocol.Page.enableReturnValue,
Protocol.Page.getFrameTreeReturnValue
>([
this._client.send('Page.enable'),
this._client.send('Page.getFrameTree'),
]);
const { frameTree } = result[1];
this._handleFrameTree(frameTree);
await Promise.all([
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client
.send('Runtime.enable', {})
.then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._networkManager.initialize(),
]);
}
networkManager(): NetworkManager {
return this._networkManager;
}
async navigateFrame(
frame: Frame,
url: string,
options: {
referer?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<Response | null> {
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();
async function navigate(
client: CDPSession,
url: string,
referrer: string,
frameId: string
): Promise<Error | null> {
try {
const response = await client.send('Page.navigate', {
url,
referrer,
frameId,
});
ensureNewDocumentNavigation = !!response.loaderId;
return response.errorText
? new Error(`${response.errorText} at ${url}`)
: null;
} catch (error) {
return error;
}
}
}
async waitForFrameNavigation(
frame: Frame,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<Response | null> {
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();
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload): void {
const frame = this._frames.get(event.frameId);
if (!frame) return;
frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(Events.FrameManager.LifecycleEvent, frame);
}
_onFrameStoppedLoading(frameId: string): void {
const frame = this._frames.get(frameId);
if (!frame) return;
frame._onLoadingStopped();
this.emit(Events.FrameManager.LifecycleEvent, frame);
}
_handleFrameTree(frameTree: Protocol.Page.FrameTree): void {
if (frameTree.frame.parentId)
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
this._onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames) return;
for (const child of frameTree.childFrames) this._handleFrameTree(child);
}
page(): Page {
return this._page;
}
mainFrame(): Frame {
return this._mainFrame;
}
frames(): Frame[] {
return Array.from(this._frames.values());
}
frame(frameId: string): Frame | null {
return this._frames.get(frameId) || null;
}
_onFrameAttached(frameId: string, parentFrameId?: string): void {
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);
}
_onFrameNavigated(framePayload: Protocol.Page.Frame): void {
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);
}
async _ensureIsolatedWorld(name: string): Promise<void> {
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,
})
.catch(debugError)
)
); // frames might be removed before we send this
}
_onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this._frames.get(frameId);
if (!frame) return;
frame._navigatedWithinDocument(url);
this.emit(Events.FrameManager.FrameNavigatedWithinDocument, frame);
this.emit(Events.FrameManager.FrameNavigated, frame);
}
_onFrameDetached(frameId: string): void {
const frame = this._frames.get(frameId);
if (frame) this._removeFramesRecursively(frame);
}
_onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription
): void {
const auxData = contextPayload.auxData as { frameId?: string };
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
let world = null;
if (frame) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld;
} else if (
contextPayload.name === UTILITY_WORLD_NAME &&
!frame._secondaryWorld._hasContext()
) {
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
world = frame._secondaryWorld;
}
}
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context = new ExecutionContext(this._client, contextPayload, world);
if (world) world._setContext(context);
this._contextIdToContext.set(contextPayload.id, context);
}
/**
* @param {number} executionContextId
*/
_onExecutionContextDestroyed(executionContextId: number): void {
const context = this._contextIdToContext.get(executionContextId);
if (!context) return;
this._contextIdToContext.delete(executionContextId);
if (context._world) context._world._setContext(null);
}
_onExecutionContextsCleared(): void {
for (const context of this._contextIdToContext.values()) {
if (context._world) context._world._setContext(null);
}
this._contextIdToContext.clear();
}
executionContextById(contextId: number): ExecutionContext {
const context = this._contextIdToContext.get(contextId);
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
return context;
}
_removeFramesRecursively(frame: Frame): void {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
frame._detach();
this._frames.delete(frame._id);
this.emit(Events.FrameManager.FrameDetached, frame);
}
}
export class Frame {
_frameManager: FrameManager;
_client: CDPSession;
_parentFrame?: Frame;
_id: string;
_url = '';
_detached = false;
_loaderId = '';
_name?: string;
_lifecycleEvents = new Set<string>();
_mainWorld: DOMWorld;
_secondaryWorld: DOMWorld;
_childFrames: Set<Frame>;
constructor(
frameManager: FrameManager,
client: CDPSession,
parentFrame: Frame | null,
frameId: string
) {
this._frameManager = frameManager;
this._client = client;
this._parentFrame = parentFrame;
this._url = '';
this._id = frameId;
this._detached = false;
this._loaderId = '';
this._mainWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._secondaryWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._childFrames = new Set();
if (this._parentFrame) this._parentFrame._childFrames.add(this);
}
async goto(
url: string,
options: {
referer?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<Response | null> {
return await this._frameManager.navigateFrame(this, url, options);
}
async waitForNavigation(options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<Response | null> {
return await this._frameManager.waitForFrameNavigation(this, options);
}
executionContext(): Promise<ExecutionContext> {
return this._mainWorld.executionContext();
}
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return this._mainWorld.evaluateHandle(pageFunction, ...args);
}
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.evaluate<ReturnType>(pageFunction, ...args);
}
async $(selector: string): Promise<ElementHandle | null> {
return this._mainWorld.$(selector);
}
async $x(expression: string): Promise<ElementHandle[]> {
return this._mainWorld.$x(expression);
}
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args);
}
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args);
}
async $$(selector: string): Promise<ElementHandle[]> {
return this._mainWorld.$$(selector);
}
async content(): Promise<string> {
return this._secondaryWorld.content();
}
async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {}
): Promise<void> {
return this._secondaryWorld.setContent(html, options);
}
name(): string {
return this._name || '';
}
url(): string {
return this._url;
}
parentFrame(): Frame | null {
return this._parentFrame;
}
childFrames(): Frame[] {
return Array.from(this._childFrames);
}
isDetached(): boolean {
return this._detached;
}
async addScriptTag(options: {
url?: string;
path?: string;
content?: string;
type?: string;
}): Promise<ElementHandle> {
return this._mainWorld.addScriptTag(options);
}
async addStyleTag(options: {
url?: string;
path?: string;
content?: string;
}): Promise<ElementHandle> {
return this._mainWorld.addStyleTag(options);
}
async click(
selector: string,
options: { delay?: number; button?: MouseButtonInput; clickCount?: number }
): Promise<void> {
return this._secondaryWorld.click(selector, options);
}
async focus(selector: string): Promise<void> {
return this._secondaryWorld.focus(selector);
}
async hover(selector: string): Promise<void> {
return this._secondaryWorld.hover(selector);
}
select(selector: string, ...values: string[]): Promise<string[]> {
return this._secondaryWorld.select(selector, ...values);
}
async tap(selector: string): Promise<void> {
return this._secondaryWorld.tap(selector);
}
async type(
selector: string,
text: string,
options?: { delay: number }
): Promise<void> {
return this._mainWorld.type(selector, text, options);
}
waitFor(
selectorOrFunctionOrTimeout: string | number | Function,
options: {} = {},
...args: unknown[]
): Promise<JSHandle | null> {
const xPathPattern = '//';
if (helper.isString(selectorOrFunctionOrTimeout)) {
const 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, selectorOrFunctionOrTimeout)
);
if (typeof selectorOrFunctionOrTimeout === 'function')
return this.waitForFunction(
selectorOrFunctionOrTimeout,
options,
...args
);
return Promise.reject(
new Error(
'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout
)
);
}
async waitForSelector(
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForSelector(
selector,
options
);
if (!handle) return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
async waitForXPath(
xpath: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle) return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
waitForFunction(
pageFunction: Function | string,
options: { polling?: string | number; timeout?: number } = {},
...args: unknown[]
): Promise<JSHandle> {
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
}
async title(): Promise<string> {
return this._secondaryWorld.title();
}
_navigated(framePayload: Protocol.Page.Frame): void {
this._name = framePayload.name;
this._url = framePayload.url;
}
_navigatedWithinDocument(url: string): void {
this._url = url;
}
_onLifecycleEvent(loaderId: string, name: string): void {
if (name === 'init') {
this._loaderId = loaderId;
this._lifecycleEvents.clear();
}
this._lifecycleEvents.add(name);
}
_onLoadingStopped(): void {
this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load');
}
_detach(): void {
this._detached = true;
this._mainWorld._detach();
this._secondaryWorld._detach();
if (this._parentFrame) this._parentFrame._childFrames.delete(this);
this._parentFrame = null;
}
}
function assertNoLegacyNavigationOptions(options: {
[optionName: string]: unknown;
}): void {
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'
);
}

Просмотреть файл

@ -14,33 +14,27 @@
* limitations under the License.
*/
const {assert} = require('./helper');
const keyDefinitions = require('./USKeyboardLayout');
import { assert } from './helper';
import { CDPSession } from './Connection';
import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout';
/**
* @typedef {Object} KeyDescription
* @property {number} keyCode
* @property {string} key
* @property {string} text
* @property {string} code
* @property {number} location
*/
type KeyDescription = Required<
Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
>;
class Keyboard {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
export class Keyboard {
_client: CDPSession;
_modifiers = 0;
_pressedKeys = new Set<string>();
constructor(client: CDPSession) {
this._client = client;
this._modifiers = 0;
this._pressedKeys = new Set();
}
/**
* @param {string} key
* @param {{text?: string}=} options
*/
async down(key, options = { text: undefined }) {
async down(
key: KeyInput,
options: { text?: string } = { text: undefined }
): Promise<void> {
const description = this._keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
@ -58,78 +52,54 @@ class Keyboard {
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3
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;
private _modifierBit(key: string): number {
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) {
private _keyDescriptionForString(keyString: KeyInput): KeyDescription {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0
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.key) description.key = definition.key;
if (shift && definition.shiftKey) description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (definition.keyCode) description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.code) description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (definition.location) description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
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 (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 (this._modifiers & ~8) description.text = '';
return description;
}
/**
* @param {string} key
*/
async up(key) {
async up(key: KeyInput): Promise<void> {
const description = this._keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
@ -140,69 +110,72 @@ class Keyboard {
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location
location: description.location,
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
await this._client.send('Input.insertText', {text: char});
async sendCharacter(char: string): Promise<void> {
await this._client.send('Input.insertText', { text: char });
}
/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options) {
private charIsKey(char: string): char is KeyInput {
return !!keyDefinitions[char];
}
async type(text: string, options: { delay?: number }): Promise<void> {
const delay = (options && options.delay) || null;
for (const char of text) {
if (keyDefinitions[char]) {
await this.press(char, {delay});
if (this.charIsKey(char)) {
await this.press(char, { delay });
} else {
if (delay)
await new Promise(f => setTimeout(f, delay));
if (delay) await new Promise((f) => setTimeout(f, delay));
await this.sendCharacter(char);
}
}
}
/**
* @param {string} key
* @param {!{delay?: number, text?: string}=} options
*/
async press(key, options = {}) {
const {delay = null} = options;
async press(
key: KeyInput,
options: { delay?: number; text?: string } = {}
): Promise<void> {
const { delay = null } = options;
await this.down(key, options);
if (delay)
await new Promise(f => setTimeout(f, options.delay));
if (delay) await new Promise((f) => setTimeout(f, options.delay));
await this.up(key);
}
}
class Mouse {
type MouseButton = 'none' | 'left' | 'right' | 'middle';
export type MouseButtonInput = Exclude<MouseButton, 'none'>;
interface MouseOptions {
button?: MouseButtonInput;
clickCount?: number;
}
export class Mouse {
_client: CDPSession;
_keyboard: Keyboard;
_x = 0;
_y = 0;
_button: MouseButton = 'none';
/**
* @param {Puppeteer.CDPSession} client
* @param {CDPSession} client
* @param {!Keyboard} keyboard
*/
constructor(client, keyboard) {
constructor(client: CDPSession, keyboard: 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;
async move(
x: number,
y: number,
options: { steps?: number } = {}
): Promise<void> {
const { steps = 1 } = options;
const fromX = this._x,
fromY = this._y;
this._x = x;
this._y = y;
for (let i = 1; i <= steps; i++) {
@ -211,24 +184,20 @@ class Mouse {
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers
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;
async click(
x: number,
y: number,
options: MouseOptions & { delay?: number } = {}
): Promise<void> {
const { delay = null } = options;
if (delay !== null) {
await Promise.all([
this.move(x, y),
this.down(options),
]);
await new Promise(f => setTimeout(f, delay));
await Promise.all([this.move(x, y), this.down(options)]);
await new Promise((f) => setTimeout(f, delay));
await this.up(options);
} else {
await Promise.all([
@ -239,11 +208,8 @@ class Mouse {
}
}
/**
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async down(options = {}) {
const {button = 'left', clickCount = 1} = options;
async down(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options;
this._button = button;
await this._client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
@ -251,15 +217,15 @@ class Mouse {
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount
clickCount,
});
}
/**
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async up(options = {}) {
const {button = 'left', clickCount = 1} = options;
async up(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options;
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
@ -267,17 +233,16 @@ class Mouse {
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount
clickCount,
});
}
}
class Touchscreen {
/**
* @param {Puppeteer.CDPSession} client
* @param {Keyboard} keyboard
*/
constructor(client, keyboard) {
export class Touchscreen {
_client: CDPSession;
_keyboard: Keyboard;
constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client;
this._keyboard = keyboard;
}
@ -286,27 +251,26 @@ class Touchscreen {
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
async tap(x: number, y: number): Promise<void> {
// 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
expression:
'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true,
});
const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints,
modifiers: this._keyboard._modifiers
modifiers: this._keyboard._modifiers,
});
await this._client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
modifiers: this._keyboard._modifiers
modifiers: this._keyboard._modifiers,
});
}
}
module.exports = { Keyboard, Mouse, Touchscreen};

Просмотреть файл

@ -0,0 +1,636 @@
/**
* 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.
*/
import { helper, assert, debugError } from './helper';
import { ExecutionContext } from './ExecutionContext';
import { Page } from './Page';
import { CDPSession } from './Connection';
import { KeyInput } from './USKeyboardLayout';
import { FrameManager, Frame } from './FrameManager';
import { getQueryHandlerAndSelector } from './QueryHandler';
interface BoxModel {
content: Array<{ x: number; y: number }>;
padding: Array<{ x: number; y: number }>;
border: Array<{ x: number; y: number }>;
margin: Array<{ x: number; y: number }>;
width: number;
height: number;
}
export function createJSHandle(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle {
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);
}
export class JSHandle {
_context: ExecutionContext;
_client: CDPSession;
_remoteObject: Protocol.Runtime.RemoteObject;
_disposed = false;
constructor(
context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject
) {
this._context = context;
this._client = client;
this._remoteObject = remoteObject;
}
executionContext(): ExecutionContext {
return this._context;
}
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this.executionContext().evaluate<ReturnType>(
pageFunction,
this,
...args
);
}
async evaluateHandle(
pageFunction: Function | string,
...args: unknown[]
): Promise<JSHandle> {
return await this.executionContext().evaluateHandle(
pageFunction,
this,
...args
);
}
async getProperty(propertyName: string): Promise<JSHandle | undefined> {
const objectHandle = await this.evaluateHandle(
(object: HTMLElement, propertyName: string) => {
const result = { __proto__: null };
result[propertyName] = object[propertyName];
return result;
},
propertyName
);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
return result;
}
async getProperties(): Promise<Map<string, JSHandle>> {
const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId,
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable) continue;
result.set(property.name, createJSHandle(this._context, property.value));
}
return result;
}
async jsonValue(): Promise<{}> {
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);
}
/* This always returns null but children can define this and return an ElementHandle */
asElement(): ElementHandle | null {
return null;
}
async dispose(): Promise<void> {
if (this._disposed) return;
this._disposed = true;
await helper.releaseObject(this._client, this._remoteObject);
}
toString(): string {
if (this._remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type;
return 'JSHandle@' + type;
}
return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
}
}
export class ElementHandle extends JSHandle {
_page: Page;
_frameManager: FrameManager;
constructor(
context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject,
page: Page,
frameManager: FrameManager
) {
super(context, client, remoteObject);
this._client = client;
this._remoteObject = remoteObject;
this._page = page;
this._frameManager = frameManager;
}
asElement(): ElementHandle | null {
return this;
}
async contentFrame(): Promise<Frame | null> {
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(): Promise<void> {
const error = await this.evaluate<Promise<string | false>>(
async (element: HTMLElement, pageJavascriptEnabled: boolean) => {
if (!element.isConnected) return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
// force-scroll if page's javascript is disabled.
if (!pageJavascriptEnabled) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
return false;
}
const visibleRatio = await new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
if (visibleRatio !== 1.0) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// Chrome still supports behavior: instant but it's not in the spec so TS shouts
// We don't want to make this breaking change in Puppeteer yet so we'll ignore the line.
// @ts-ignore
behavior: 'instant',
});
}
return false;
},
this._page._javascriptEnabled
);
if (error) throw new Error(error);
}
async _clickablePoint(): Promise<{ x: number; y: number }> {
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,
};
}
_getBoxModel(): Promise<void | Protocol.DOM.getBoxModelReturnValue> {
return this._client
.send('DOM.getBoxModel', {
objectId: this._remoteObject.objectId,
})
.catch((error) => debugError(error));
}
_fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> {
return [
{ x: quad[0], y: quad[1] },
{ x: quad[2], y: quad[3] },
{ x: quad[4], y: quad[5] },
{ x: quad[6], y: quad[7] },
];
}
_intersectQuadWithViewport(
quad: Array<{ x: number; y: number }>,
width: number,
height: number
): Array<{ x: number; y: number }> {
return quad.map((point) => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
}
async hover(): Promise<void> {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.mouse.move(x, y);
}
async click(options: {
delay?: number;
button?: 'left' | 'right' | 'middle';
clickCount?: number;
}): Promise<void> {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.mouse.click(x, y, options);
}
async select(...values: string[]): Promise<string[]> {
for (const value of values)
assert(
helper.isString(value),
'Values must be strings. Found value "' +
value +
'" of type "' +
typeof value +
'"'
);
/* TODO(jacktfranklin@): once ExecutionContext is TypeScript, and
* its evaluate function is properly typed with generics we can
* return here and remove the typecasting
*/
return this.evaluate((element: HTMLSelectElement, values: string[]) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple) break;
}
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return options
.filter((option) => option.selected)
.map((option) => option.value);
}, values);
}
async uploadFile(...filePaths: string[]): Promise<void> {
const isMultiple = await this.evaluate<boolean>(
(element: HTMLInputElement) => element.multiple
);
assert(
filePaths.length <= 1 || isMultiple,
'Multiple file uploads only work with <input type=file multiple>'
);
// This import is only needed for `uploadFile`, so keep it scoped here to avoid paying
// the cost unnecessarily.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { promisify } = require('util');
const access = promisify(fs.access);
// Locate all files and confirm that they exist.
const files = await Promise.all(
filePaths.map(async (filePath) => {
const resolvedPath: string = path.resolve(filePath);
try {
await access(resolvedPath, fs.constants.R_OK);
} catch (error) {
if (error.code === 'ENOENT')
throw new Error(`${filePath} does not exist or is not readable`);
}
return resolvedPath;
})
);
const { objectId } = this._remoteObject;
const { node } = await this._client.send('DOM.describeNode', { objectId });
const { backendNodeId } = node;
// The zero-length array is a special case, it seems that DOM.setFileInputFiles does
// not actually update the files in that case, so the solution is to eval the element
// value to a new FileList directly.
if (files.length === 0) {
await this.evaluate((element: HTMLInputElement) => {
element.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
});
} else {
await this._client.send('DOM.setFileInputFiles', {
objectId,
files,
backendNodeId,
});
}
}
async tap(): Promise<void> {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this._clickablePoint();
await this._page.touchscreen.tap(x, y);
}
async focus(): Promise<void> {
await this.evaluate((element) => element.focus());
}
async type(text: string, options?: { delay: number }): Promise<void> {
await this.focus();
await this._page.keyboard.type(text, options);
}
async press(
key: KeyInput,
options?: { delay?: number; text?: string }
): Promise<void> {
await this.focus();
await this._page.keyboard.press(key, options);
}
async boundingBox(): Promise<{
x: number;
y: number;
width: number;
height: number;
}> {
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(): Promise<BoxModel | null> {
const result = await this._getBoxModel();
if (!result) return null;
const { content, padding, border, margin, width, height } = result.model;
return {
content: this._fromProtocolQuad(content),
padding: this._fromProtocolQuad(padding),
border: this._fromProtocolQuad(border),
margin: this._fromProtocolQuad(margin),
width,
height,
};
}
async screenshot(options = {}): Promise<string | Buffer | void> {
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;
}
async $(selector: string): Promise<ElementHandle | null> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelector(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const handle = await this.evaluateHandle(queryHandler, updatedSelector);
const element = handle.asElement();
if (element) return element;
await handle.dispose();
return null;
}
async $$(selector: string): Promise<ElementHandle[]> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelectorAll(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const arrayHandle = await this.evaluateHandle(
queryHandler,
updatedSelector
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle) result.push(elementHandle);
}
return result;
}
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(
`Error: failed to find element matching selector "${selector}"`
);
const result = await elementHandle.evaluate<ReturnType>(
pageFunction,
...args
);
await elementHandle.dispose();
return result;
}
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
const defaultHandler = (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector));
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
);
const arrayHandle = await this.evaluateHandle(
queryHandler,
updatedSelector
);
const result = await arrayHandle.evaluate<ReturnType>(
pageFunction,
...args
);
await arrayHandle.dispose();
return result;
}
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle((element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(
expression,
element,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array = [];
let item;
while ((item = iterator.iterateNext())) array.push(item);
return array;
}, expression);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle) result.push(elementHandle);
}
return result;
}
async isIntersectingViewport(): Promise<boolean> {
return await this.evaluate<Promise<boolean>>(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;
});
}
}
function computeQuadArea(quad: Array<{ x: number; y: number }>): number {
// 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);
}

Просмотреть файл

@ -0,0 +1,803 @@
/**
* 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.
*/
import * as os from 'os';
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import * as URL from 'url';
import * as fs from 'fs';
import { BrowserFetcher } from './BrowserFetcher';
import { Connection } from './Connection';
import { Browser } from './Browser';
import { helper, assert, debugError } from './helper';
import type { ConnectionTransport } from './ConnectionTransport';
import { WebSocketTransport } from './WebSocketTransport';
import { BrowserRunner } from './launcher/BrowserRunner';
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const writeFileAsync = helper.promisify(fs.writeFile);
import type {
ChromeArgOptions,
LaunchOptions,
BrowserOptions,
} from './launcher/LaunchOptions';
export interface ProductLauncher {
launch(object);
connect(object);
executablePath: () => string;
defaultArgs(object);
product: string;
}
class ChromeLauncher implements ProductLauncher {
_projectRoot: string;
_preferredRevision: string;
_isPuppeteerCore: boolean;
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(
options: LaunchOptions & ChromeArgOptions & BrowserOptions = {}
): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
dumpio = false,
executablePath = null,
pipe = false,
env = process.env,
handleSIGINT = true,
handleSIGTERM = true,
handleSIGHUP = true,
ignoreHTTPSErrors = false,
defaultViewport = { width: 800, height: 600 },
slowMo = 0,
timeout = 30000,
} = options;
const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
const chromeArguments = [];
if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
chromeArguments.push(
...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg)
)
);
else chromeArguments.push(...args);
let temporaryUserDataDir = null;
if (
!chromeArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
)
)
chromeArguments.push(
pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0'
);
if (!chromeArguments.some((arg) => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(profilePath);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
}
let chromeExecutable = executablePath;
if (!executablePath) {
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText) throw new Error(missingText);
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner(
chromeExecutable,
chromeArguments,
temporaryUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
});
try {
const connection = await runner.setupConnection({
usePipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser.create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
throw error;
}
}
/**
* @param {!Launcher.ChromeArgOptions=} options
* @return {!Array<string>}
*/
defaultArgs(options: ChromeArgOptions = {}): string[] {
const chromeArguments = [
'--disable-background-networking',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=TranslateUI',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null,
} = options;
if (userDataDir) chromeArguments.push(`--user-data-dir=${userDataDir}`);
if (devtools) chromeArguments.push('--auto-open-devtools-for-tabs');
if (headless) {
chromeArguments.push('--headless', '--hide-scrollbars', '--mute-audio');
}
if (args.every((arg) => arg.startsWith('-')))
chromeArguments.push('about:blank');
chromeArguments.push(...args);
return chromeArguments;
}
executablePath(): string {
return resolveExecutablePath(this).executablePath;
}
get product(): string {
return 'chrome';
}
async connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> {
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)
);
}
}
class FirefoxLauncher implements ProductLauncher {
_projectRoot: string;
_preferredRevision: string;
_isPuppeteerCore: boolean;
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(
options: LaunchOptions &
ChromeArgOptions &
BrowserOptions & {
extraPrefsFirefox?: { [x: string]: unknown };
} = {}
): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
dumpio = false,
executablePath = null,
pipe = false,
env = process.env,
handleSIGINT = true,
handleSIGTERM = true,
handleSIGHUP = true,
ignoreHTTPSErrors = false,
defaultViewport = { width: 800, height: 600 },
slowMo = 0,
timeout = 30000,
extraPrefsFirefox = {},
} = options;
const firefoxArguments = [];
if (!ignoreDefaultArgs) firefoxArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
firefoxArguments.push(
...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg)
)
);
else firefoxArguments.push(...args);
if (
!firefoxArguments.some((argument) =>
argument.startsWith('--remote-debugging-')
)
)
firefoxArguments.push('--remote-debugging-port=0');
let temporaryUserDataDir = null;
if (
!firefoxArguments.includes('-profile') &&
!firefoxArguments.includes('--profile')
) {
temporaryUserDataDir = await this._createProfile(extraPrefsFirefox);
firefoxArguments.push('--profile');
firefoxArguments.push(temporaryUserDataDir);
}
await this._updateRevision();
let firefoxExecutable = executablePath;
if (!executablePath) {
const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText) throw new Error(missingText);
firefoxExecutable = executablePath;
}
const runner = new BrowserRunner(
firefoxExecutable,
firefoxArguments,
temporaryUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe,
});
try {
const connection = await runner.setupConnection({
usePipe: pipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
const browser = await Browser.create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
await browser.waitForTarget((t) => t.type() === 'page');
return browser;
} catch (error) {
runner.kill();
throw error;
}
}
async connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> {
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)
);
}
executablePath(): string {
return resolveExecutablePath(this).executablePath;
}
async _updateRevision(): Promise<void> {
// replace 'latest' placeholder with actual downloaded revision
if (this._preferredRevision === 'latest') {
const browserFetcher = new BrowserFetcher(this._projectRoot, {
product: this.product,
});
const localRevisions = await browserFetcher.localRevisions();
if (localRevisions[0]) this._preferredRevision = localRevisions[0];
}
}
get product(): string {
return 'firefox';
}
defaultArgs(options: ChromeArgOptions = {}): string[] {
const firefoxArguments = ['--no-remote', '--foreground'];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null,
} = options;
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
if (headless) firefoxArguments.push('--headless');
if (devtools) firefoxArguments.push('--devtools');
if (args.every((arg) => arg.startsWith('-')))
firefoxArguments.push('about:blank');
firefoxArguments.push(...args);
return firefoxArguments;
}
async _createProfile(extraPrefs: { [x: string]: unknown }): Promise<string> {
const profilePath = await mkdtempAsync(
path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-')
);
const prefsJS = [];
const userJS = [];
const server = 'dummy.test';
const defaultPreferences = {
// Make sure Shield doesn't hit the network.
'app.normandy.api_url': '',
// Disable Firefox old build background check
'app.update.checkInstallTime': false,
// Disable automatically upgrading Firefox
'app.update.disabledForTesting': true,
// Increase the APZ content response timeout to 1 minute
'apz.content_response_timeout': 60000,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'browser.contentblocking.features.standard':
'-tp,tpPrivate,cookieBehavior0,-cm,-fp',
// Enable the dump function: which sends messages to the system
// console
// https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
'browser.dom.window.dump.enabled': true,
// Disable topstories
'browser.newtabpage.activity-stream.feeds.section.topstories': false,
// Always display a blank page
'browser.newtabpage.enabled': false,
// Background thumbnails in particular cause grief: and disabling
// thumbnails in general cannot hurt
'browser.pagethumbnails.capturing_disabled': true,
// Disable safebrowsing components.
'browser.safebrowsing.blockedURIs.enabled': false,
'browser.safebrowsing.downloads.enabled': false,
'browser.safebrowsing.malware.enabled': false,
'browser.safebrowsing.passwords.enabled': false,
'browser.safebrowsing.phishing.enabled': false,
// Disable updates to search engines.
'browser.search.update': false,
// Do not restore the last open set of tabs if the browser has crashed
'browser.sessionstore.resume_from_crash': false,
// Skip check for default browser on startup
'browser.shell.checkDefaultBrowser': false,
// Disable newtabpage
'browser.startup.homepage': 'about:blank',
// Do not redirect user when a milstone upgrade of Firefox is detected
'browser.startup.homepage_override.mstone': 'ignore',
// Start with a blank page about:blank
'browser.startup.page': 0,
// Do not allow background tabs to be zombified on Android: otherwise for
// tests that open additional tabs: the test harness tab itself might get
// unloaded
'browser.tabs.disableBackgroundZombification': false,
// Do not warn when closing all other open tabs
'browser.tabs.warnOnCloseOtherTabs': false,
// Do not warn when multiple tabs will be opened
'browser.tabs.warnOnOpen': false,
// Disable the UI tour.
'browser.uitour.enabled': false,
// Turn off search suggestions in the location bar so as not to trigger
// network connections.
'browser.urlbar.suggest.searches': false,
// Disable first run splash page on Windows 10
'browser.usedOnWindows10.introURL': '',
// Do not warn on quitting Firefox
'browser.warnOnQuit': false,
// Do not show datareporting policy notifications which can
// interfere with tests
'datareporting.healthreport.about.reportUrl': `http://${server}/dummy/abouthealthreport/`,
'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
'datareporting.healthreport.logging.consoleEnabled': false,
'datareporting.healthreport.service.enabled': false,
'datareporting.healthreport.service.firstRun': false,
'datareporting.healthreport.uploadEnabled': false,
'datareporting.policy.dataSubmissionEnabled': false,
'datareporting.policy.dataSubmissionPolicyAccepted': false,
'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
// This doesn't affect Puppeteer but spams console (Bug 1424372)
'devtools.jsonview.enabled': false,
// Disable popup-blocker
'dom.disable_open_during_load': false,
// Enable the support for File object creation in the content process
// Required for |Page.setFileInputFiles| protocol method.
'dom.file.createInChild': true,
// Disable the ProcessHangMonitor
'dom.ipc.reportProcessHangs': false,
// Disable slow script dialogues
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
'extensions.enabledScopes': 5,
// Disable metadata caching for installed add-ons by default
'extensions.getAddons.cache.enabled': false,
// Disable installing any distribution extensions or add-ons.
'extensions.installDistroAddons': false,
// Disabled screenshots extension
'extensions.screenshots.disabled': true,
// Turn off extension updates so they do not bother tests
'extensions.update.enabled': false,
// Turn off extension updates so they do not bother tests
'extensions.update.notifyUser': false,
// Make sure opening about:addons will not hit the network
'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
// Allow the application to have focus even it runs in the background
'focusmanager.testmode': true,
// Disable useragent updates
'general.useragent.updates.enabled': false,
// Always use network provider for geolocation tests so we bypass the
// macOS dialog raised by the corelocation provider
'geo.provider.testing': true,
// Do not scan Wifi
'geo.wifi.scan': false,
// No hang monitor
'hangmonitor.timeout': 0,
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,
// Prevent various error message on the console
// jest-puppeteer asserts that no error message is emitted by the console
'network.cookie.cookieBehavior': 0,
// Do not prompt for temporary redirects
'network.http.prompt-temp-redirect': false,
// Disable speculative connections so they are not reported as leaking
// when they are hanging around
'network.http.speculative-parallel-limit': 0,
// Do not automatically switch between offline and online
'network.manage-offline-status': false,
// Make sure SNTP requests do not hit the network
'network.sntp.pools': server,
// Disable Flash.
'plugin.state.flash': 0,
'privacy.trackingprotection.enabled': false,
// Enable Remote Agent
// https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
'remote.enabled': true,
// Don't do network connections for mitm priming
'security.certerrors.mitm.priming.enabled': false,
// Local documents have access to all other local documents,
// including directory listings
'security.fileuri.strict_origin_policy': false,
// Do not wait for the notification button security delay
'security.notification_enable_delay': 0,
// Ensure blocklist updates do not hit the network
'services.settings.server': `http://${server}/dummy/blocklist/`,
// Do not automatically fill sign-in forms with known usernames and
// passwords
'signon.autofillForms': false,
// Disable password capture, so that tests that include forms are not
// influenced by the presence of the persistent doorhanger notification
'signon.rememberSignons': false,
// Disable first-run welcome page
'startup.homepage_welcome_url': 'about:blank',
// Disable first-run welcome page
'startup.homepage_welcome_url.additional': '',
// Disable browser animations (tabs, fullscreen, sliding alerts)
'toolkit.cosmeticAnimations.enabled': false,
// We want to collect telemetry, but we don't want to send in the results
'toolkit.telemetry.server': `https://${server}/dummy/telemetry/`,
// Prevent starting into safe mode after application crashes
'toolkit.startup.max_resumed_crashes': -1,
};
Object.assign(defaultPreferences, extraPrefs);
for (const [key, value] of Object.entries(defaultPreferences))
userJS.push(
`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`
);
await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n'));
await writeFileAsync(
path.join(profilePath, 'prefs.js'),
prefsJS.join('\n')
);
return profilePath;
}
}
function getWSEndpoint(browserURL: string): Promise<string> {
let resolve, reject;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
const endpointURL = URL.resolve(browserURL, '/json/version');
const protocol = endpointURL.startsWith('https') ? https : http;
const requestOptions = Object.assign(URL.parse(endpointURL), {
method: 'GET',
});
const request = protocol.request(requestOptions, (res) => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
res.resume();
reject(new Error('HTTP ' + res.statusCode));
return;
}
res.setEncoding('utf8');
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl));
});
request.on('error', reject);
request.end();
return promise.catch((error) => {
error.message =
`Failed to fetch browser webSocket url from ${endpointURL}: ` +
error.message;
throw error;
});
}
function resolveExecutablePath(
launcher: ChromeLauncher | FirefoxLauncher
): { executablePath: string; missingText?: string } {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!launcher._isPuppeteerCore) {
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.npm_config_puppeteer_executable_path ||
process.env.npm_package_config_puppeteer_executable_path;
if (executablePath) {
const missingText = !fs.existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: null;
return { executablePath, missingText };
}
}
const browserFetcher = new BrowserFetcher(launcher._projectRoot, {
product: launcher.product,
});
if (!launcher._isPuppeteerCore && launcher.product === 'chrome') {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local
? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' +
revisionInfo.executablePath
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
}
const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
const missingText = !revisionInfo.local
? `Could not find browser revision ${launcher._preferredRevision}. Run "npm install" or "yarn install" to download a browser binary.`
: null;
return { executablePath: revisionInfo.executablePath, missingText };
}
function Launcher(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean,
product?: string
): ProductLauncher {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore)
product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
switch (product) {
case 'firefox':
return new FirefoxLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
case 'chrome':
default:
if (typeof product !== 'undefined' && product !== 'chrome') {
/* The user gave us an incorrect product name
* we'll default to launching Chrome, but log to the console
* to let the user know (they've probably typoed).
*/
console.warn(
`Warning: unknown product name ${product}. Falling back to chrome.`
);
}
return new ChromeLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
}
}
export default Launcher;

Просмотреть файл

@ -0,0 +1,235 @@
/**
* 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.
*/
import { helper, assert, PuppeteerEventListener } from './helper';
import { Events } from './Events';
import { TimeoutError } from './Errors';
import { FrameManager, Frame } from './FrameManager';
import { Request } from './Request';
import { Response } from './Response';
export type PuppeteerLifeCycleEvent =
| 'load'
| 'domcontentloaded'
| 'networkidle0'
| 'networkidle2';
type ProtocolLifeCycleEvent =
| 'load'
| 'DOMContentLoaded'
| 'networkIdle'
| 'networkAlmostIdle';
const puppeteerToProtocolLifecycle = new Map<
PuppeteerLifeCycleEvent,
ProtocolLifeCycleEvent
>([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);
export class LifecycleWatcher {
_expectedLifecycle: ProtocolLifeCycleEvent[];
_frameManager: FrameManager;
_frame: Frame;
_timeout: number;
_navigationRequest?: Request;
_eventListeners: PuppeteerEventListener[];
_initialLoaderId: string;
_sameDocumentNavigationPromise: Promise<Error | null>;
_sameDocumentNavigationCompleteCallback: (x?: Error) => void;
_lifecyclePromise: Promise<void>;
_lifecycleCallback: () => void;
_newDocumentNavigationPromise: Promise<Error | null>;
_newDocumentNavigationCompleteCallback: (x?: Error) => void;
_terminationPromise: Promise<Error | null>;
_terminationCallback: (x?: Error) => void;
_timeoutPromise: Promise<TimeoutError | null>;
_maximumTimer?: NodeJS.Timeout;
_hasSameDocumentNavigation?: boolean;
constructor(
frameManager: FrameManager,
frame: Frame,
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
timeout: number
) {
if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice();
else if (typeof waitUntil === 'string') waitUntil = [waitUntil];
this._expectedLifecycle = waitUntil.map((value) => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent;
});
this._frameManager = frameManager;
this._frame = frame;
this._initialLoaderId = frame._loaderId;
this._timeout = timeout;
this._navigationRequest = null;
this._eventListeners = [
helper.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<Error | null>(
(fulfill) => {
this._sameDocumentNavigationCompleteCallback = fulfill;
}
);
this._lifecyclePromise = new Promise((fulfill) => {
this._lifecycleCallback = fulfill;
});
this._newDocumentNavigationPromise = new Promise((fulfill) => {
this._newDocumentNavigationCompleteCallback = fulfill;
});
this._timeoutPromise = this._createTimeoutPromise();
this._terminationPromise = new Promise((fulfill) => {
this._terminationCallback = fulfill;
});
this._checkLifecycleComplete();
}
_onRequest(request: Request): void {
if (request.frame() !== this._frame || !request.isNavigationRequest())
return;
this._navigationRequest = request;
}
_onFrameDetached(frame: Frame): void {
if (this._frame === frame) {
this._terminationCallback.call(
null,
new Error('Navigating frame was detached')
);
return;
}
this._checkLifecycleComplete();
}
navigationResponse(): Response | null {
return this._navigationRequest ? this._navigationRequest.response() : null;
}
_terminate(error: Error): void {
this._terminationCallback.call(null, error);
}
sameDocumentNavigationPromise(): Promise<Error | null> {
return this._sameDocumentNavigationPromise;
}
newDocumentNavigationPromise(): Promise<Error | null> {
return this._newDocumentNavigationPromise;
}
lifecyclePromise(): Promise<void> {
return this._lifecyclePromise;
}
timeoutOrTerminationPromise(): Promise<Error | TimeoutError | null> {
return Promise.race([this._timeoutPromise, this._terminationPromise]);
}
_createTimeoutPromise(): Promise<TimeoutError | null> {
if (!this._timeout) return new Promise(() => {});
const errorMessage =
'Navigation timeout of ' + this._timeout + ' ms exceeded';
return new Promise(
(fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout))
).then(() => new TimeoutError(errorMessage));
}
_navigatedWithinDocument(frame: Frame): void {
if (frame !== this._frame) return;
this._hasSameDocumentNavigation = true;
this._checkLifecycleComplete();
}
_checkLifecycleComplete(): void {
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle)) return;
this._lifecycleCallback();
if (
this._frame._loaderId === this._initialLoaderId &&
!this._hasSameDocumentNavigation
)
return;
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
if (this._frame._loaderId !== this._initialLoaderId)
this._newDocumentNavigationCompleteCallback();
/**
* @param {!Frame} frame
* @param {!Array<string>} expectedLifecycle
* @return {boolean}
*/
function checkLifecycle(
frame: Frame,
expectedLifecycle: ProtocolLifeCycleEvent[]
): boolean {
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(): void {
helper.removeEventListeners(this._eventListeners);
clearTimeout(this._maximumTimer);
}
}

Просмотреть файл

@ -0,0 +1,322 @@
/**
* 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.
*/
import * as EventEmitter from 'events';
import { helper, assert, debugError } from './helper';
import { Events } from './Events';
import { CDPSession } from './Connection';
import { FrameManager } from './FrameManager';
import { Request } from './Request';
import { Response } from './Response';
export interface Credentials {
username: string;
password: string;
}
export class NetworkManager extends EventEmitter {
_client: CDPSession;
_ignoreHTTPSErrors: boolean;
_frameManager: FrameManager;
_requestIdToRequest = new Map<string, Request>();
_requestIdToRequestWillBeSentEvent = new Map<
string,
Protocol.Network.requestWillBeSentPayload
>();
_extraHTTPHeaders: Record<string, string> = {};
_offline = false;
_credentials?: Credentials = null;
_attemptedAuthentications = new Set<string>();
_userRequestInterceptionEnabled = false;
_protocolRequestInterceptionEnabled = false;
_userCacheDisabled = false;
_requestIdToInterceptionId = new Map<string, string>();
constructor(
client: CDPSession,
ignoreHTTPSErrors: boolean,
frameManager: FrameManager
) {
super();
this._client = client;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._frameManager = frameManager;
this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
this._client.on(
'Network.requestWillBeSent',
this._onRequestWillBeSent.bind(this)
);
this._client.on(
'Network.requestServedFromCache',
this._onRequestServedFromCache.bind(this)
);
this._client.on(
'Network.responseReceived',
this._onResponseReceived.bind(this)
);
this._client.on(
'Network.loadingFinished',
this._onLoadingFinished.bind(this)
);
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
}
async initialize(): Promise<void> {
await this._client.send('Network.enable');
if (this._ignoreHTTPSErrors)
await this._client.send('Security.setIgnoreCertificateErrors', {
ignore: true,
});
}
async authenticate(credentials?: Credentials): Promise<void> {
this._credentials = credentials;
await this._updateProtocolRequestInterception();
}
async setExtraHTTPHeaders(
extraHTTPHeaders: Record<string, string>
): Promise<void> {
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,
});
}
extraHTTPHeaders(): Record<string, string> {
return Object.assign({}, this._extraHTTPHeaders);
}
async setOfflineMode(value: boolean): Promise<void> {
if (this._offline === value) return;
this._offline = value;
await this._client.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
}
async setUserAgent(userAgent: string): Promise<void> {
await this._client.send('Network.setUserAgentOverride', { userAgent });
}
async setCacheEnabled(enabled: boolean): Promise<void> {
this._userCacheDisabled = !enabled;
await this._updateProtocolCacheDisabled();
}
async setRequestInterception(value: boolean): Promise<void> {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
}
async _updateProtocolRequestInterception(): Promise<void> {
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(): Promise<void> {
await this._client.send('Network.setCacheDisabled', {
cacheDisabled:
this._userCacheDisabled || this._protocolRequestInterceptionEnabled,
});
}
_onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload): void {
// 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: Protocol.Fetch.authRequiredPayload): void {
/* TODO(jacktfranklin): This is defined in protocol.d.ts but not
* in an easily referrable way - we should look at exposing it.
*/
type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials';
let response: AuthResponse = '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);
}
_onRequestPaused(event: Protocol.Fetch.requestPausedPayload): void {
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);
}
}
_onRequest(
event: Protocol.Network.requestWillBeSentPayload,
interceptionId?: string
): void {
let redirectChain = [];
if (event.redirectResponse) {
const request = this._requestIdToRequest.get(event.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) {
this._handleRequestRedirect(request, event.redirectResponse);
redirectChain = request._redirectChain;
}
}
const frame = event.frameId
? this._frameManager.frame(event.frameId)
: null;
const request = new Request(
this._client,
frame,
interceptionId,
this._userRequestInterceptionEnabled,
event,
redirectChain
);
this._requestIdToRequest.set(event.requestId, request);
this.emit(Events.NetworkManager.Request, request);
}
_onRequestServedFromCache(
event: Protocol.Network.requestServedFromCachePayload
): void {
const request = this._requestIdToRequest.get(event.requestId);
if (request) request._fromMemoryCache = true;
}
_handleRequestRedirect(
request: Request,
responsePayload: Protocol.Network.Response
): void {
const response = new Response(this._client, request, responsePayload);
request._response = response;
request._redirectChain.push(request);
response._resolveBody(
new Error('Response body is unavailable for redirect responses')
);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events.NetworkManager.Response, response);
this.emit(Events.NetworkManager.RequestFinished, request);
}
_onResponseReceived(event: Protocol.Network.responseReceivedPayload): void {
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);
}
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload): void {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) return;
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
if (request.response()) request.response()._resolveBody(null);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events.NetworkManager.RequestFinished, request);
}
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload): void {
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) return;
request._failureText = event.errorText;
const response = request.response();
if (response) response._resolveBody(null);
this._requestIdToRequest.delete(request._requestId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(Events.NetworkManager.RequestFailed, request);
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -13,24 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper, debugError} = require('./helper');
import { helper, debugError, PuppeteerEventListener } from './helper';
import type { ConnectionTransport } from './ConnectionTransport';
/**
* @implements {!Puppeteer.ConnectionTransport}
*/
class PipeTransport {
/**
* @param {!NodeJS.WritableStream} pipeWrite
* @param {!NodeJS.ReadableStream} pipeRead
*/
constructor(pipeWrite, pipeRead) {
export class PipeTransport implements ConnectionTransport {
_pipeWrite: NodeJS.WritableStream;
_pendingMessage: string;
_eventListeners: PuppeteerEventListener[];
onclose?: () => void;
onmessage?: () => void;
constructor(
pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream
) {
this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
helper.addEventListener(pipeRead, 'data', (buffer) =>
this._dispatch(buffer)
),
helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose)
this.onclose.call(null);
if (this.onclose) this.onclose.call(null);
}),
helper.addEventListener(pipeRead, 'error', debugError),
helper.addEventListener(pipeWrite, 'error', debugError),
@ -39,26 +44,19 @@ class PipeTransport {
this.onclose = null;
}
/**
* @param {string} message
*/
send(message) {
send(message: string): void {
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
}
/**
* @param {!Buffer} buffer
*/
_dispatch(buffer) {
_dispatch(buffer: Buffer): void {
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);
if (this.onmessage) this.onmessage.call(null, message);
let start = end + 1;
end = buffer.indexOf('\0', start);
@ -71,10 +69,8 @@ class PipeTransport {
this._pendingMessage = buffer.toString(undefined, start);
}
close() {
close(): void {
this._pipeWrite = null;
helper.removeEventListeners(this._eventListeners);
}
}
module.exports = PipeTransport;

Просмотреть файл

@ -0,0 +1,157 @@
/**
* 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.
*/
import Launcher from './Launcher';
import type {
LaunchOptions,
ChromeArgOptions,
BrowserOptions,
} from './launcher/LaunchOptions';
import type { ProductLauncher } from './Launcher';
import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher';
import { puppeteerErrors, PuppeteerErrors } from './Errors';
import type { ConnectionTransport } from './ConnectionTransport';
import { devicesMap } from './DeviceDescriptors';
import type { DevicesMap } from './/DeviceDescriptors';
import { Browser } from './Browser';
import * as QueryHandler from './QueryHandler';
export class Puppeteer {
_projectRoot: string;
_preferredRevision: string;
_isPuppeteerCore: boolean;
_changedProduct = false;
__productName: string;
_lazyLauncher: ProductLauncher;
constructor(
projectRoot: string,
preferredRevision: string,
isPuppeteerCore: boolean,
productName: string
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
// track changes to Launcher configuration via options or environment variables
this.__productName = productName;
}
launch(
options: LaunchOptions &
ChromeArgOptions &
BrowserOptions & { product?: string; extraPrefsFirefox?: {} } = {}
): Promise<Browser> {
if (options.product) this._productName = options.product;
return this._launcher.launch(options);
}
connect(
options: BrowserOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
product?: string;
}
): Promise<Browser> {
if (options.product) this._productName = options.product;
return this._launcher.connect(options);
}
set _productName(name: string) {
if (this.__productName !== name) this._changedProduct = true;
this.__productName = name;
}
get _productName(): string {
return this.__productName;
}
executablePath(): string {
return this._launcher.executablePath();
}
get _launcher(): ProductLauncher {
if (
!this._lazyLauncher ||
this._lazyLauncher.product !== this._productName ||
this._changedProduct
) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
switch (this._productName) {
case 'firefox':
this._preferredRevision = packageJson.puppeteer.firefox_revision;
break;
case 'chrome':
default:
this._preferredRevision = packageJson.puppeteer.chromium_revision;
}
this._changedProduct = false;
this._lazyLauncher = Launcher(
this._projectRoot,
this._preferredRevision,
this._isPuppeteerCore,
this._productName
);
}
return this._lazyLauncher;
}
get product(): string {
return this._launcher.product;
}
get devices(): DevicesMap {
return devicesMap;
}
get errors(): PuppeteerErrors {
return puppeteerErrors;
}
defaultArgs(options: ChromeArgOptions): string[] {
return this._launcher.defaultArgs(options);
}
createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher {
return new BrowserFetcher(this._projectRoot, options);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_registerCustomQueryHandler(
name: string,
queryHandler: QueryHandler.QueryHandler
): void {
QueryHandler.registerCustomQueryHandler(name, queryHandler);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_unregisterCustomQueryHandler(name: string): void {
QueryHandler.unregisterCustomQueryHandler(name);
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_customQueryHandlers(): Map<string, QueryHandler.QueryHandler> {
return QueryHandler.customQueryHandlers();
}
// eslint-disable-next-line @typescript-eslint/camelcase
__experimental_clearQueryHandlers(): void {
QueryHandler.clearQueryHandlers();
}
}

Просмотреть файл

@ -1,5 +1,5 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,23 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {TestRunner, Reporter, Matchers} = require('..');
const runner = new TestRunner();
const reporter = new Reporter(runner);
const {expect} = new Matchers();
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
beforeAll(() => {
expect(false).toBeTruthy();
});
it('test', async () => {
});
});
runner.run();
export interface Viewport {
width: number;
height: number;
deviceScaleFactor?: number;
isMobile?: boolean;
isLandscape?: boolean;
hasTouch?: boolean;
}

Просмотреть файл

@ -0,0 +1,84 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface QueryHandler {
(element: Element | Document, selector: string):
| Element
| Element[]
| NodeListOf<Element>;
}
const _customQueryHandlers = new Map<string, QueryHandler>();
export function registerCustomQueryHandler(
name: string,
handler: Function
): void {
if (_customQueryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`);
const isValidName = /^[a-zA-Z]+$/.test(name);
if (!isValidName)
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
_customQueryHandlers.set(name, handler as QueryHandler);
}
/**
* @param {string} name
*/
export function unregisterCustomQueryHandler(name: string): void {
_customQueryHandlers.delete(name);
}
export function customQueryHandlers(): Map<string, QueryHandler> {
return _customQueryHandlers;
}
export function clearQueryHandlers(): void {
_customQueryHandlers.clear();
}
export function getQueryHandlerAndSelector(
selector: string,
defaultQueryHandler: QueryHandler
): { updatedSelector: string; queryHandler: QueryHandler } {
const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector);
if (!hasCustomQueryHandler)
return { updatedSelector: selector, queryHandler: defaultQueryHandler };
const index = selector.indexOf('/');
const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1);
const queryHandler = customQueryHandlers().get(name);
if (!queryHandler)
throw new Error(
`Query set to use "${name}", but no query handler of that name was found`
);
return {
updatedSelector,
queryHandler,
};
}
module.exports = {
registerCustomQueryHandler,
unregisterCustomQueryHandler,
customQueryHandlers,
getQueryHandlerAndSelector,
clearQueryHandlers,
};

Просмотреть файл

@ -0,0 +1,316 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CDPSession } from './Connection';
import { Frame } from './FrameManager';
import { Response } from './Response';
import { helper, assert, debugError } from './helper';
export class Request {
_requestId: string;
_interceptionId: string;
_failureText = null;
_response: Response | null = null;
_fromMemoryCache = false;
_redirectChain: Request[];
private _client: CDPSession;
private _isNavigationRequest: boolean;
private _allowInterception: boolean;
private _interceptionHandled = false;
private _url: string;
private _resourceType: string;
private _method: string;
private _postData?: string;
private _headers: Record<string, string> = {};
private _frame: Frame;
constructor(
client: CDPSession,
frame: Frame,
interceptionId: string,
allowInterception: boolean,
event: Protocol.Network.requestWillBeSentPayload,
redirectChain: Request[]
) {
this._client = client;
this._requestId = event.requestId;
this._isNavigationRequest =
event.requestId === event.loaderId && event.type === 'Document';
this._interceptionId = interceptionId;
this._allowInterception = allowInterception;
this._url = event.request.url;
this._resourceType = event.type.toLowerCase();
this._method = event.request.method;
this._postData = event.request.postData;
this._frame = frame;
this._redirectChain = redirectChain;
for (const key of Object.keys(event.request.headers))
this._headers[key.toLowerCase()] = event.request.headers[key];
}
url(): string {
return this._url;
}
resourceType(): string {
return this._resourceType;
}
method(): string {
return this._method;
}
postData(): string | undefined {
return this._postData;
}
headers(): Record<string, string> {
return this._headers;
}
response(): Response | null {
return this._response;
}
frame(): Frame | null {
return this._frame;
}
isNavigationRequest(): boolean {
return this._isNavigationRequest;
}
redirectChain(): Request[] {
return this._redirectChain.slice();
}
/**
* @return {?{errorText: string}}
*/
failure(): { errorText: string } | null {
if (!this._failureText) return null;
return {
errorText: this._failureText,
};
}
async continue(
overrides: {
url?: string;
method?: string;
postData?: string;
headers?: Record<string, string>;
} = {}
): Promise<void> {
// 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);
});
}
async respond(response: {
status: number;
headers: Record<string, string>;
contentType: string;
body: string | Buffer;
}): Promise<void> {
// 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: Buffer | null =
response.body && helper.isString(response.body)
? Buffer.from(response.body)
: (response.body as Buffer) || null;
const responseHeaders: Record<string, string> = {};
if (response.headers) {
for (const header of Object.keys(response.headers))
responseHeaders[header.toLowerCase()] = response.headers[header];
}
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(
Buffer.byteLength(responseBody)
);
await this._client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: response.status || 200,
responsePhrase: STATUS_TEXTS[response.status || 200],
responseHeaders: headersArray(responseHeaders),
body: responseBody ? responseBody.toString('base64') : undefined,
})
.catch((error) => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
}
async abort(errorCode: ErrorCode = 'failed'): Promise<void> {
// 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);
});
}
}
type ErrorCode =
| 'aborted'
| 'accessdenied'
| 'addressunreachable'
| 'blockedbyclient'
| 'blockedbyresponse'
| 'connectionaborted'
| 'connectionclosed'
| 'connectionfailed'
| 'connectionrefused'
| 'connectionreset'
| 'internetdisconnected'
| 'namenotresolved'
| 'timedout'
| 'failed';
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
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',
} as const;
function headersArray(
headers: Record<string, string>
): Array<{ name: string; value: string }> {
const result = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined))
result.push({ name, value: headers[name] + '' });
}
return result;
}
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
const STATUS_TEXTS = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
} as const;

Просмотреть файл

@ -0,0 +1,142 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CDPSession } from './Connection';
import { Frame } from './FrameManager';
import { Request } from './Request';
import { SecurityDetails } from './SecurityDetails';
interface RemoteAddress {
ip: string;
port: number;
}
export class Response {
private _client: CDPSession;
private _request: Request;
private _contentPromise: Promise<Buffer> | null = null;
private _bodyLoadedPromise: Promise<Error | void>;
private _bodyLoadedPromiseFulfill: (err: Error | void) => void;
private _remoteAddress: RemoteAddress;
private _status: number;
private _statusText: string;
private _url: string;
private _fromDiskCache: boolean;
private _fromServiceWorker: boolean;
private _headers: Record<string, string> = {};
private _securityDetails: SecurityDetails | null;
constructor(
client: CDPSession,
request: Request,
responsePayload: Protocol.Network.Response
) {
this._client = client;
this._request = request;
this._bodyLoadedPromise = new Promise((fulfill) => {
this._bodyLoadedPromiseFulfill = fulfill;
});
this._remoteAddress = {
ip: responsePayload.remoteIPAddress,
port: responsePayload.remotePort,
};
this._status = responsePayload.status;
this._statusText = responsePayload.statusText;
this._url = request.url();
this._fromDiskCache = !!responsePayload.fromDiskCache;
this._fromServiceWorker = !!responsePayload.fromServiceWorker;
for (const key of Object.keys(responsePayload.headers))
this._headers[key.toLowerCase()] = responsePayload.headers[key];
this._securityDetails = responsePayload.securityDetails
? new SecurityDetails(responsePayload.securityDetails)
: null;
}
_resolveBody(err: Error | null): void {
return this._bodyLoadedPromiseFulfill(err);
}
remoteAddress(): RemoteAddress {
return this._remoteAddress;
}
url(): string {
return this._url;
}
ok(): boolean {
return this._status === 0 || (this._status >= 200 && this._status <= 299);
}
status(): number {
return this._status;
}
statusText(): string {
return this._statusText;
}
headers(): Record<string, string> {
return this._headers;
}
securityDetails(): SecurityDetails | null {
return this._securityDetails;
}
buffer(): Promise<Buffer> {
if (!this._contentPromise) {
this._contentPromise = this._bodyLoadedPromise.then(async (error) => {
if (error) throw error;
const response = await this._client.send('Network.getResponseBody', {
requestId: this._request._requestId,
});
return Buffer.from(
response.body,
response.base64Encoded ? 'base64' : 'utf8'
);
});
}
return this._contentPromise;
}
async text(): Promise<string> {
const content = await this.buffer();
return content.toString('utf8');
}
async json(): Promise<any> {
const content = await this.text();
return JSON.parse(content);
}
request(): Request {
return this._request;
}
fromCache(): boolean {
return this._fromDiskCache || this._request._fromMemoryCache;
}
fromServiceWorker(): boolean {
return this._fromServiceWorker;
}
frame(): Frame | null {
return this._request.frame();
}
}

Просмотреть файл

@ -0,0 +1,57 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class SecurityDetails {
private _subjectName: string;
private _issuer: string;
private _validFrom: number;
private _validTo: number;
private _protocol: string;
private _sanList: string[];
constructor(securityPayload: Protocol.Network.SecurityDetails) {
this._subjectName = securityPayload.subjectName;
this._issuer = securityPayload.issuer;
this._validFrom = securityPayload.validFrom;
this._validTo = securityPayload.validTo;
this._protocol = securityPayload.protocol;
this._sanList = securityPayload.sanList;
}
subjectName(): string {
return this._subjectName;
}
issuer(): string {
return this._issuer;
}
validFrom(): number {
return this._validFrom;
}
validTo(): number {
return this._validTo;
}
protocol(): string {
return this._protocol;
}
subjectAlternativeNames(): string[] {
return this._sanList;
}
}

Просмотреть файл

@ -0,0 +1,169 @@
/**
* 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.
*/
import { Events } from './Events';
import { Page } from './Page';
import { Worker as PuppeteerWorker } from './Worker';
import { CDPSession } from './Connection';
import { Browser, BrowserContext } from './Browser';
import type { Viewport } from './PuppeteerViewport';
export class Target {
_targetInfo: Protocol.Target.TargetInfo;
_browserContext: BrowserContext;
_targetId: string;
_sessionFactory: () => Promise<CDPSession>;
_ignoreHTTPSErrors: boolean;
_defaultViewport?: Viewport;
_pagePromise?: Promise<Page>;
_workerPromise?: Promise<PuppeteerWorker>;
_initializedPromise: Promise<boolean>;
_initializedCallback: (x: boolean) => void;
_isClosedPromise: Promise<boolean>;
_closedCallback: () => void;
_isInitialized: boolean;
constructor(
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext,
sessionFactory: () => Promise<CDPSession>,
ignoreHTTPSErrors: boolean,
defaultViewport: Viewport | null
) {
this._targetInfo = targetInfo;
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
this._sessionFactory = sessionFactory;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
/** @type {?Promise<!Puppeteer.Page>} */
this._pagePromise = null;
/** @type {?Promise<!PuppeteerWorker>} */
this._workerPromise = null;
this._initializedPromise = new Promise<boolean>(
(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<boolean>(
(fulfill) => (this._closedCallback = fulfill)
);
this._isInitialized =
this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized) this._initializedCallback(true);
}
createCDPSession(): Promise<CDPSession> {
return this._sessionFactory();
}
async page(): Promise<Page | null> {
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
)
);
}
return this._pagePromise;
}
async worker(): Promise<PuppeteerWorker | null> {
if (
this._targetInfo.type !== 'service_worker' &&
this._targetInfo.type !== 'shared_worker'
)
return null;
if (!this._workerPromise) {
// TODO(einbinder): Make workers send their console logs.
this._workerPromise = this._sessionFactory().then(
(client) =>
new PuppeteerWorker(
client,
this._targetInfo.url,
() => {} /* consoleAPICalled */,
() => {} /* exceptionThrown */
)
);
}
return this._workerPromise;
}
url(): string {
return this._targetInfo.url;
}
type():
| 'page'
| 'background_page'
| 'service_worker'
| 'shared_worker'
| 'other'
| 'browser' {
const type = this._targetInfo.type;
if (
type === 'page' ||
type === 'background_page' ||
type === 'service_worker' ||
type === 'shared_worker' ||
type === 'browser'
)
return type;
return 'other';
}
browser(): Browser {
return this._browserContext.browser();
}
browserContext(): BrowserContext {
return this._browserContext;
}
opener(): Target | null {
const { openerId } = this._targetInfo;
if (!openerId) return null;
return this.browser()._targets.get(openerId);
}
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
this._targetInfo = targetInfo;
if (
!this._isInitialized &&
(this._targetInfo.type !== 'page' || this._targetInfo.url !== '')
) {
this._isInitialized = true;
this._initializedCallback(true);
return;
}
}
}

Просмотреть файл

@ -16,42 +16,35 @@
const DEFAULT_TIMEOUT = 30000;
class TimeoutSettings {
export class TimeoutSettings {
_defaultTimeout: number | null;
_defaultNavigationTimeout: number | null;
constructor() {
this._defaultTimeout = null;
this._defaultNavigationTimeout = null;
}
/**
* @param {number} timeout
*/
setDefaultTimeout(timeout) {
setDefaultTimeout(timeout: number): void {
this._defaultTimeout = timeout;
}
/**
* @param {number} timeout
*/
setDefaultNavigationTimeout(timeout) {
setDefaultNavigationTimeout(timeout: number): void {
this._defaultNavigationTimeout = timeout;
}
/**
* @return {number}
*/
navigationTimeout() {
navigationTimeout(): number {
if (this._defaultNavigationTimeout !== null)
return this._defaultNavigationTimeout;
if (this._defaultTimeout !== null)
return this._defaultTimeout;
if (this._defaultTimeout !== null) return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
timeout() {
if (this._defaultTimeout !== null)
return this._defaultTimeout;
timeout(): number {
if (this._defaultTimeout !== null) return this._defaultTimeout;
return DEFAULT_TIMEOUT;
}
}
module.exports = {TimeoutSettings};

Просмотреть файл

@ -0,0 +1,82 @@
/**
* 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.
*/
import { helper, assert } from './helper';
import { CDPSession } from './Connection';
interface TracingOptions {
path?: string;
screenshots?: boolean;
categories?: string[];
}
export class Tracing {
_client: CDPSession;
_recording = false;
_path = '';
constructor(client: CDPSession) {
this._client = client;
}
async start(options: TracingOptions = {}): Promise<void> {
assert(
!this._recording,
'Cannot start recording trace while already recording trace.'
);
const defaultCategories = [
'-*',
'devtools.timeline',
'v8.execute',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame',
'toplevel',
'blink.console',
'blink.user_timing',
'latencyInfo',
'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-v8.cpu_profiler.hires',
];
const {
path = null,
screenshots = false,
categories = defaultCategories,
} = options;
if (screenshots) categories.push('disabled-by-default-devtools.screenshot');
this._path = path;
this._recording = true;
await this._client.send('Tracing.start', {
transferMode: 'ReturnAsStream',
categories: categories.join(','),
});
}
async stop(): Promise<Buffer> {
let fulfill: (value: Buffer) => void;
const contentPromise = new Promise<Buffer>((x) => (fulfill = x));
this._client.once('Tracing.tracingComplete', (event) => {
helper
.readProtocolStream(this._client, event.stream, this._path)
.then(fulfill);
});
await this._client.send('Tracing.end');
this._recording = false;
return contentPromise;
}
}

Просмотреть файл

@ -0,0 +1,669 @@
/**
* 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.
*/
export interface KeyDefinition {
keyCode?: number;
shiftKeyCode?: number;
key?: string;
shiftKey?: string;
code?: string;
text?: string;
shiftText?: string;
location?: number;
}
export type KeyInput =
| '0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| 'Power'
| 'Eject'
| 'Abort'
| 'Help'
| 'Backspace'
| 'Tab'
| 'Numpad5'
| 'NumpadEnter'
| 'Enter'
| '\r'
| '\n'
| 'ShiftLeft'
| 'ShiftRight'
| 'ControlLeft'
| 'ControlRight'
| 'AltLeft'
| 'AltRight'
| 'Pause'
| 'CapsLock'
| 'Escape'
| 'Convert'
| 'NonConvert'
| 'Space'
| 'Numpad9'
| 'PageUp'
| 'Numpad3'
| 'PageDown'
| 'End'
| 'Numpad1'
| 'Home'
| 'Numpad7'
| 'ArrowLeft'
| 'Numpad4'
| 'Numpad8'
| 'ArrowUp'
| 'ArrowRight'
| 'Numpad6'
| 'Numpad2'
| 'ArrowDown'
| 'Select'
| 'Open'
| 'PrintScreen'
| 'Insert'
| 'Numpad0'
| 'Delete'
| 'NumpadDecimal'
| 'Digit0'
| 'Digit1'
| 'Digit2'
| 'Digit3'
| 'Digit4'
| 'Digit5'
| 'Digit6'
| 'Digit7'
| 'Digit8'
| 'Digit9'
| 'KeyA'
| 'KeyB'
| 'KeyC'
| 'KeyD'
| 'KeyE'
| 'KeyF'
| 'KeyG'
| 'KeyH'
| 'KeyI'
| 'KeyJ'
| 'KeyK'
| 'KeyL'
| 'KeyM'
| 'KeyN'
| 'KeyO'
| 'KeyP'
| 'KeyQ'
| 'KeyR'
| 'KeyS'
| 'KeyT'
| 'KeyU'
| 'KeyV'
| 'KeyW'
| 'KeyX'
| 'KeyY'
| 'KeyZ'
| 'MetaLeft'
| 'MetaRight'
| 'ContextMenu'
| 'NumpadMultiply'
| 'NumpadAdd'
| 'NumpadSubtract'
| 'NumpadDivide'
| 'F1'
| 'F2'
| 'F3'
| 'F4'
| 'F5'
| 'F6'
| 'F7'
| 'F8'
| 'F9'
| 'F10'
| 'F11'
| 'F12'
| 'F13'
| 'F14'
| 'F15'
| 'F16'
| 'F17'
| 'F18'
| 'F19'
| 'F20'
| 'F21'
| 'F22'
| 'F23'
| 'F24'
| 'NumLock'
| 'ScrollLock'
| 'AudioVolumeMute'
| 'AudioVolumeDown'
| 'AudioVolumeUp'
| 'MediaTrackNext'
| 'MediaTrackPrevious'
| 'MediaStop'
| 'MediaPlayPause'
| 'Semicolon'
| 'Equal'
| 'NumpadEqual'
| 'Comma'
| 'Minus'
| 'Period'
| 'Slash'
| 'Backquote'
| 'BracketLeft'
| 'Backslash'
| 'BracketRight'
| 'Quote'
| 'AltGraph'
| 'Props'
| 'Cancel'
| 'Clear'
| 'Shift'
| 'Control'
| 'Alt'
| 'Accept'
| 'ModeChange'
| ' '
| 'Print'
| 'Execute'
| '\u0000'
| 'a'
| 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'i'
| 'j'
| 'k'
| 'l'
| 'm'
| 'n'
| 'o'
| 'p'
| 'q'
| 'r'
| 's'
| 't'
| 'u'
| 'v'
| 'w'
| 'x'
| 'y'
| 'z'
| 'Meta'
| '*'
| '+'
| '-'
| '/'
| ';'
| '='
| ','
| '.'
| '`'
| '['
| '\\'
| ']'
| "'"
| 'Attn'
| 'CrSel'
| 'ExSel'
| 'EraseEof'
| 'Play'
| 'ZoomOut'
| ')'
| '!'
| '@'
| '#'
| '$'
| '%'
| '^'
| '&'
| '('
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'W'
| 'X'
| 'Y'
| 'Z'
| ':'
| '<'
| '_'
| '>'
| '?'
| '~'
| '{'
| '|'
| '}'
| '"'
| 'SoftLeft'
| 'SoftRight'
| 'Camera'
| 'Call'
| 'EndCall'
| 'VolumeDown'
| 'VolumeUp';
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
'0': { keyCode: 48, key: '0', code: 'Digit0' },
'1': { keyCode: 49, key: '1', code: 'Digit1' },
'2': { keyCode: 50, key: '2', code: 'Digit2' },
'3': { keyCode: 51, key: '3', code: 'Digit3' },
'4': { keyCode: 52, key: '4', code: 'Digit4' },
'5': { keyCode: 53, key: '5', code: 'Digit5' },
'6': { keyCode: 54, key: '6', code: 'Digit6' },
'7': { keyCode: 55, key: '7', code: 'Digit7' },
'8': { keyCode: 56, key: '8', code: 'Digit8' },
'9': { keyCode: 57, key: '9', code: 'Digit9' },
Power: { key: 'Power', code: 'Power' },
Eject: { key: 'Eject', code: 'Eject' },
Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' },
Help: { keyCode: 6, code: 'Help', key: 'Help' },
Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },
Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },
Numpad5: {
keyCode: 12,
shiftKeyCode: 101,
key: 'Clear',
code: 'Numpad5',
shiftKey: '5',
location: 3,
},
NumpadEnter: {
keyCode: 13,
code: 'NumpadEnter',
key: 'Enter',
text: '\r',
location: 3,
},
Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
'\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 },
ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 },
ControlLeft: {
keyCode: 17,
code: 'ControlLeft',
key: 'Control',
location: 1,
},
ControlRight: {
keyCode: 17,
code: 'ControlRight',
key: 'Control',
location: 2,
},
AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 },
AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 },
Pause: { keyCode: 19, code: 'Pause', key: 'Pause' },
CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },
Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },
Convert: { keyCode: 28, code: 'Convert', key: 'Convert' },
NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' },
Space: { keyCode: 32, code: 'Space', key: ' ' },
Numpad9: {
keyCode: 33,
shiftKeyCode: 105,
key: 'PageUp',
code: 'Numpad9',
shiftKey: '9',
location: 3,
},
PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' },
Numpad3: {
keyCode: 34,
shiftKeyCode: 99,
key: 'PageDown',
code: 'Numpad3',
shiftKey: '3',
location: 3,
},
PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' },
End: { keyCode: 35, code: 'End', key: 'End' },
Numpad1: {
keyCode: 35,
shiftKeyCode: 97,
key: 'End',
code: 'Numpad1',
shiftKey: '1',
location: 3,
},
Home: { keyCode: 36, code: 'Home', key: 'Home' },
Numpad7: {
keyCode: 36,
shiftKeyCode: 103,
key: 'Home',
code: 'Numpad7',
shiftKey: '7',
location: 3,
},
ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
Numpad4: {
keyCode: 37,
shiftKeyCode: 100,
key: 'ArrowLeft',
code: 'Numpad4',
shiftKey: '4',
location: 3,
},
Numpad8: {
keyCode: 38,
shiftKeyCode: 104,
key: 'ArrowUp',
code: 'Numpad8',
shiftKey: '8',
location: 3,
},
ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
Numpad6: {
keyCode: 39,
shiftKeyCode: 102,
key: 'ArrowRight',
code: 'Numpad6',
shiftKey: '6',
location: 3,
},
Numpad2: {
keyCode: 40,
shiftKeyCode: 98,
key: 'ArrowDown',
code: 'Numpad2',
shiftKey: '2',
location: 3,
},
ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
Select: { keyCode: 41, code: 'Select', key: 'Select' },
Open: { keyCode: 43, code: 'Open', key: 'Execute' },
PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },
Insert: { keyCode: 45, code: 'Insert', key: 'Insert' },
Numpad0: {
keyCode: 45,
shiftKeyCode: 96,
key: 'Insert',
code: 'Numpad0',
shiftKey: '0',
location: 3,
},
Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },
NumpadDecimal: {
keyCode: 46,
shiftKeyCode: 110,
code: 'NumpadDecimal',
key: '\u0000',
shiftKey: '.',
location: 3,
},
Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' },
Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' },
Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' },
Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' },
Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' },
Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' },
Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' },
Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' },
Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' },
Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' },
KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' },
KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' },
KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' },
KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' },
KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' },
KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' },
KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' },
KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' },
KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' },
KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' },
KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' },
KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' },
KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' },
KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' },
KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' },
KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' },
KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' },
KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' },
KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' },
KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' },
KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' },
KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' },
KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' },
KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' },
KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' },
KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' },
MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 },
MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 },
ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },
NumpadMultiply: {
keyCode: 106,
code: 'NumpadMultiply',
key: '*',
location: 3,
},
NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 },
NumpadSubtract: {
keyCode: 109,
code: 'NumpadSubtract',
key: '-',
location: 3,
},
NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 },
F1: { keyCode: 112, code: 'F1', key: 'F1' },
F2: { keyCode: 113, code: 'F2', key: 'F2' },
F3: { keyCode: 114, code: 'F3', key: 'F3' },
F4: { keyCode: 115, code: 'F4', key: 'F4' },
F5: { keyCode: 116, code: 'F5', key: 'F5' },
F6: { keyCode: 117, code: 'F6', key: 'F6' },
F7: { keyCode: 118, code: 'F7', key: 'F7' },
F8: { keyCode: 119, code: 'F8', key: 'F8' },
F9: { keyCode: 120, code: 'F9', key: 'F9' },
F10: { keyCode: 121, code: 'F10', key: 'F10' },
F11: { keyCode: 122, code: 'F11', key: 'F11' },
F12: { keyCode: 123, code: 'F12', key: 'F12' },
F13: { keyCode: 124, code: 'F13', key: 'F13' },
F14: { keyCode: 125, code: 'F14', key: 'F14' },
F15: { keyCode: 126, code: 'F15', key: 'F15' },
F16: { keyCode: 127, code: 'F16', key: 'F16' },
F17: { keyCode: 128, code: 'F17', key: 'F17' },
F18: { keyCode: 129, code: 'F18', key: 'F18' },
F19: { keyCode: 130, code: 'F19', key: 'F19' },
F20: { keyCode: 131, code: 'F20', key: 'F20' },
F21: { keyCode: 132, code: 'F21', key: 'F21' },
F22: { keyCode: 133, code: 'F22', key: 'F22' },
F23: { keyCode: 134, code: 'F23', key: 'F23' },
F24: { keyCode: 135, code: 'F24', key: 'F24' },
NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' },
ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },
AudioVolumeMute: {
keyCode: 173,
code: 'AudioVolumeMute',
key: 'AudioVolumeMute',
},
AudioVolumeDown: {
keyCode: 174,
code: 'AudioVolumeDown',
key: 'AudioVolumeDown',
},
AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' },
MediaTrackNext: {
keyCode: 176,
code: 'MediaTrackNext',
key: 'MediaTrackNext',
},
MediaTrackPrevious: {
keyCode: 177,
code: 'MediaTrackPrevious',
key: 'MediaTrackPrevious',
},
MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' },
MediaPlayPause: {
keyCode: 179,
code: 'MediaPlayPause',
key: 'MediaPlayPause',
},
Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' },
Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' },
NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 },
Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' },
Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' },
Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' },
Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' },
Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' },
BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' },
Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' },
BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' },
Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: "'" },
AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' },
Props: { keyCode: 247, code: 'Props', key: 'CrSel' },
Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' },
Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 },
Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 },
Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 },
Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 },
Accept: { keyCode: 30, key: 'Accept' },
ModeChange: { keyCode: 31, key: 'ModeChange' },
' ': { keyCode: 32, key: ' ', code: 'Space' },
Print: { keyCode: 42, key: 'Print' },
Execute: { keyCode: 43, key: 'Execute', code: 'Open' },
'\u0000': { keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3 },
a: { keyCode: 65, key: 'a', code: 'KeyA' },
b: { keyCode: 66, key: 'b', code: 'KeyB' },
c: { keyCode: 67, key: 'c', code: 'KeyC' },
d: { keyCode: 68, key: 'd', code: 'KeyD' },
e: { keyCode: 69, key: 'e', code: 'KeyE' },
f: { keyCode: 70, key: 'f', code: 'KeyF' },
g: { keyCode: 71, key: 'g', code: 'KeyG' },
h: { keyCode: 72, key: 'h', code: 'KeyH' },
i: { keyCode: 73, key: 'i', code: 'KeyI' },
j: { keyCode: 74, key: 'j', code: 'KeyJ' },
k: { keyCode: 75, key: 'k', code: 'KeyK' },
l: { keyCode: 76, key: 'l', code: 'KeyL' },
m: { keyCode: 77, key: 'm', code: 'KeyM' },
n: { keyCode: 78, key: 'n', code: 'KeyN' },
o: { keyCode: 79, key: 'o', code: 'KeyO' },
p: { keyCode: 80, key: 'p', code: 'KeyP' },
q: { keyCode: 81, key: 'q', code: 'KeyQ' },
r: { keyCode: 82, key: 'r', code: 'KeyR' },
s: { keyCode: 83, key: 's', code: 'KeyS' },
t: { keyCode: 84, key: 't', code: 'KeyT' },
u: { keyCode: 85, key: 'u', code: 'KeyU' },
v: { keyCode: 86, key: 'v', code: 'KeyV' },
w: { keyCode: 87, key: 'w', code: 'KeyW' },
x: { keyCode: 88, key: 'x', code: 'KeyX' },
y: { keyCode: 89, key: 'y', code: 'KeyY' },
z: { keyCode: 90, key: 'z', code: 'KeyZ' },
Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 },
'*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 },
'+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 },
'-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 },
'/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 },
';': { keyCode: 186, key: ';', code: 'Semicolon' },
'=': { keyCode: 187, key: '=', code: 'Equal' },
',': { keyCode: 188, key: ',', code: 'Comma' },
'.': { keyCode: 190, key: '.', code: 'Period' },
'`': { keyCode: 192, key: '`', code: 'Backquote' },
'[': { keyCode: 219, key: '[', code: 'BracketLeft' },
'\\': { keyCode: 220, key: '\\', code: 'Backslash' },
']': { keyCode: 221, key: ']', code: 'BracketRight' },
"'": { keyCode: 222, key: "'", code: 'Quote' },
Attn: { keyCode: 246, key: 'Attn' },
CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' },
ExSel: { keyCode: 248, key: 'ExSel' },
EraseEof: { keyCode: 249, key: 'EraseEof' },
Play: { keyCode: 250, key: 'Play' },
ZoomOut: { keyCode: 251, key: 'ZoomOut' },
')': { keyCode: 48, key: ')', code: 'Digit0' },
'!': { keyCode: 49, key: '!', code: 'Digit1' },
'@': { keyCode: 50, key: '@', code: 'Digit2' },
'#': { keyCode: 51, key: '#', code: 'Digit3' },
$: { keyCode: 52, key: '$', code: 'Digit4' },
'%': { keyCode: 53, key: '%', code: 'Digit5' },
'^': { keyCode: 54, key: '^', code: 'Digit6' },
'&': { keyCode: 55, key: '&', code: 'Digit7' },
'(': { keyCode: 57, key: '(', code: 'Digit9' },
A: { keyCode: 65, key: 'A', code: 'KeyA' },
B: { keyCode: 66, key: 'B', code: 'KeyB' },
C: { keyCode: 67, key: 'C', code: 'KeyC' },
D: { keyCode: 68, key: 'D', code: 'KeyD' },
E: { keyCode: 69, key: 'E', code: 'KeyE' },
F: { keyCode: 70, key: 'F', code: 'KeyF' },
G: { keyCode: 71, key: 'G', code: 'KeyG' },
H: { keyCode: 72, key: 'H', code: 'KeyH' },
I: { keyCode: 73, key: 'I', code: 'KeyI' },
J: { keyCode: 74, key: 'J', code: 'KeyJ' },
K: { keyCode: 75, key: 'K', code: 'KeyK' },
L: { keyCode: 76, key: 'L', code: 'KeyL' },
M: { keyCode: 77, key: 'M', code: 'KeyM' },
N: { keyCode: 78, key: 'N', code: 'KeyN' },
O: { keyCode: 79, key: 'O', code: 'KeyO' },
P: { keyCode: 80, key: 'P', code: 'KeyP' },
Q: { keyCode: 81, key: 'Q', code: 'KeyQ' },
R: { keyCode: 82, key: 'R', code: 'KeyR' },
S: { keyCode: 83, key: 'S', code: 'KeyS' },
T: { keyCode: 84, key: 'T', code: 'KeyT' },
U: { keyCode: 85, key: 'U', code: 'KeyU' },
V: { keyCode: 86, key: 'V', code: 'KeyV' },
W: { keyCode: 87, key: 'W', code: 'KeyW' },
X: { keyCode: 88, key: 'X', code: 'KeyX' },
Y: { keyCode: 89, key: 'Y', code: 'KeyY' },
Z: { keyCode: 90, key: 'Z', code: 'KeyZ' },
':': { keyCode: 186, key: ':', code: 'Semicolon' },
'<': { keyCode: 188, key: '<', code: 'Comma' },
_: { keyCode: 189, key: '_', code: 'Minus' },
'>': { keyCode: 190, key: '>', code: 'Period' },
'?': { keyCode: 191, key: '?', code: 'Slash' },
'~': { keyCode: 192, key: '~', code: 'Backquote' },
'{': { keyCode: 219, key: '{', code: 'BracketLeft' },
'|': { keyCode: 220, key: '|', code: 'Backslash' },
'}': { keyCode: 221, key: '}', code: 'BracketRight' },
'"': { keyCode: 222, key: '"', code: 'Quote' },
SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 },
SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 },
Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 },
Call: { key: 'Call', code: 'Call', location: 4 },
EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 },
VolumeDown: {
keyCode: 182,
key: 'VolumeDown',
code: 'VolumeDown',
location: 4,
},
VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 },
};

Просмотреть файл

@ -13,39 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const WebSocket = require('ws');
import * as NodeWebSocket from 'ws';
import type { ConnectionTransport } from './ConnectionTransport';
/**
* @implements {!Puppeteer.ConnectionTransport}
*/
class WebSocketTransport {
/**
* @param {string} url
* @return {!Promise<!WebSocketTransport>}
*/
static create(url) {
export class WebSocketTransport implements ConnectionTransport {
static create(url: string): Promise<WebSocketTransport> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, [], {
const ws = new NodeWebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
});
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws)));
ws.addEventListener('error', reject);
});
}
/**
* @param {!WebSocket} ws
*/
constructor(ws) {
_ws: NodeWebSocket;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(ws: NodeWebSocket) {
this._ws = ws;
this._ws.addEventListener('message', event => {
if (this.onmessage)
this.onmessage.call(null, event.data);
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);
this._ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
@ -53,16 +47,11 @@ class WebSocketTransport {
this.onclose = null;
}
/**
* @param {string} message
*/
send(message) {
send(message): void {
this._ws.send(message);
}
close() {
close(): void {
this._ws.close();
}
}
module.exports = WebSocketTransport;

Просмотреть файл

@ -0,0 +1,105 @@
/**
* 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.
*/
import { EventEmitter } from 'events';
import { debugError } from './helper';
import { ExecutionContext } from './ExecutionContext';
import { JSHandle } from './JSHandle';
import { CDPSession } from './Connection';
type ConsoleAPICalledCallback = (
eventType: string,
handles: JSHandle[],
trace: Protocol.Runtime.StackTrace
) => void;
type ExceptionThrownCallback = (
details: Protocol.Runtime.ExceptionDetails
) => void;
type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
export class Worker extends EventEmitter {
_client: CDPSession;
_url: string;
_executionContextPromise: Promise<ExecutionContext>;
_executionContextCallback: (value: ExecutionContext) => void;
constructor(
client: CDPSession,
url: string,
consoleAPICalled: ConsoleAPICalledCallback,
exceptionThrown: ExceptionThrownCallback
) {
super();
this._client = client;
this._url = url;
this._executionContextPromise = new Promise<ExecutionContext>(
(x) => (this._executionContextCallback = x)
);
let jsHandleFactory: JSHandleFactory;
this._client.once('Runtime.executionContextCreated', async (event) => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
jsHandleFactory = (remoteObject) =>
new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(
client,
event.context,
null
);
this._executionContextCallback(executionContext);
});
// This might fail if the target is closed before we recieve all execution contexts.
this._client.send('Runtime.enable', {}).catch(debugError);
this._client.on('Runtime.consoleAPICalled', (event) =>
consoleAPICalled(
event.type,
event.args.map(jsHandleFactory),
event.stackTrace
)
);
this._client.on('Runtime.exceptionThrown', (exception) =>
exceptionThrown(exception.exceptionDetails)
);
}
url(): string {
return this._url;
}
async executionContext(): Promise<ExecutionContext> {
return this._executionContextPromise;
}
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: any[]
): Promise<ReturnType> {
return (await this._executionContextPromise).evaluate<ReturnType>(
pageFunction,
...args
);
}
async evaluateHandle(
pageFunction: Function | string,
...args: any[]
): Promise<JSHandle> {
return (await this._executionContextPromise).evaluateHandle(
pageFunction,
...args
);
}
}

Просмотреть файл

@ -14,30 +14,34 @@
* limitations under the License.
*/
/* This file is used in two places:
* 1) the coverage-utils use it to gain a list of all methods we check for test coverage on
* 2) index.js uses it to iterate through all methods and call helper.installAsyncStackHooks on
*/
module.exports = {
Accessibility: require('./Accessibility').Accessibility,
Browser: require('./Browser').Browser,
BrowserContext: require('./Browser').BrowserContext,
BrowserFetcher: require('./BrowserFetcher'),
BrowserFetcher: require('./BrowserFetcher').BrowserFetcher,
CDPSession: require('./Connection').CDPSession,
ConsoleMessage: require('./Page').ConsoleMessage,
ConsoleMessage: require('./ConsoleMessage').ConsoleMessage,
Coverage: require('./Coverage').Coverage,
Dialog: require('./Dialog').Dialog,
ElementHandle: require('./JSHandle').ElementHandle,
ExecutionContext: require('./ExecutionContext').ExecutionContext,
FileChooser: require('./Page').FileChooser,
FileChooser: require('./FileChooser').FileChooser,
Frame: require('./FrameManager').Frame,
JSHandle: require('./JSHandle').JSHandle,
Keyboard: require('./Input').Keyboard,
Mouse: require('./Input').Mouse,
Page: require('./Page').Page,
Puppeteer: require('./Puppeteer'),
Request: require('./NetworkManager').Request,
Response: require('./NetworkManager').Response,
SecurityDetails: require('./NetworkManager').SecurityDetails,
Puppeteer: require('./Puppeteer').Puppeteer,
Request: require('./Request').Request,
Response: require('./Response').Response,
SecurityDetails: require('./SecurityDetails').SecurityDetails,
Target: require('./Target').Target,
TimeoutError: require('./Errors').TimeoutError,
Touchscreen: require('./Input').Touchscreen,
Tracing: require('./Tracing'),
Tracing: require('./Tracing').Tracing,
Worker: require('./Worker').Worker,
};

Просмотреть файл

@ -0,0 +1,285 @@
/**
* 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.
*/
import { TimeoutError } from './Errors';
import * as debug from 'debug';
import * as fs from 'fs';
import { CDPSession } from './Connection';
import { promisify } from 'util';
const openAsync = promisify(fs.open);
const writeAsync = promisify(fs.write);
const closeAsync = promisify(fs.close);
export const debugError = debug('puppeteer:error');
export function assert(value: unknown, message?: string): void {
if (!value) throw new Error(message);
}
interface AnyClass {
prototype: object;
}
function getExceptionMessage(
exceptionDetails: Protocol.Runtime.ExceptionDetails
): string {
if (exceptionDetails.exception)
return (
exceptionDetails.exception.description || exceptionDetails.exception.value
);
let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) {
const location =
callframe.url +
':' +
callframe.lineNumber +
':' +
callframe.columnNumber;
const functionName = callframe.functionName || '<anonymous>';
message += `\n at ${functionName} (${location})`;
}
}
return message;
}
function valueFromRemoteObject(
remoteObject: Protocol.Runtime.RemoteObject
): any {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
return BigInt(remoteObject.unserializableValue.replace('n', ''));
switch (remoteObject.unserializableValue) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error(
'Unsupported unserializable value: ' +
remoteObject.unserializableValue
);
}
}
return remoteObject.value;
}
async function releaseObject(
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject
): Promise<void> {
if (!remoteObject.objectId) return;
await client
.send('Runtime.releaseObject', { objectId: remoteObject.objectId })
.catch((error) => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}
function installAsyncStackHooks(classType: AnyClass): void {
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (
methodName === 'constructor' ||
typeof methodName !== 'string' ||
methodName.startsWith('_') ||
typeof method !== 'function' ||
method.constructor.name !== 'AsyncFunction'
)
continue;
Reflect.set(classType.prototype, methodName, function (...args) {
const syncStack = {
stack: '',
};
Error.captureStackTrace(syncStack);
return method.call(this, ...args).catch((error) => {
const stack = syncStack.stack.substring(
syncStack.stack.indexOf('\n') + 1
);
const clientStack = stack.substring(stack.indexOf('\n'));
if (
error instanceof Error &&
error.stack &&
!error.stack.includes(clientStack)
)
error.stack += '\n -- ASYNC --\n' + stack;
throw error;
});
});
}
}
export interface PuppeteerEventListener {
emitter: NodeJS.EventEmitter;
eventName: string | symbol;
handler: (...args: any[]) => void;
}
function addEventListener(
emitter: NodeJS.EventEmitter,
eventName: string | symbol,
handler: (...args: any[]) => void
): PuppeteerEventListener {
emitter.on(eventName, handler);
return { emitter, eventName, handler };
}
function removeEventListeners(
listeners: Array<{
emitter: NodeJS.EventEmitter;
eventName: string | symbol;
handler: (...args: any[]) => void;
}>
): void {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
listeners.length = 0;
}
function isString(obj: unknown): obj is string {
return typeof obj === 'string' || obj instanceof String;
}
function isNumber(obj: unknown): obj is number {
return typeof obj === 'number' || obj instanceof Number;
}
async function waitForEvent<T extends any>(
emitter: NodeJS.EventEmitter,
eventName: string | symbol,
predicate: (event: T) => boolean,
timeout: number,
abortPromise: Promise<Error>
): Promise<T> {
let eventTimeout, resolveCallback, rejectCallback;
const promise = new Promise<T>((resolve, reject) => {
resolveCallback = resolve;
rejectCallback = reject;
});
const listener = addEventListener(emitter, eventName, (event) => {
if (!predicate(event)) return;
resolveCallback(event);
});
if (timeout) {
eventTimeout = setTimeout(() => {
rejectCallback(
new TimeoutError('Timeout exceeded while waiting for event')
);
}, timeout);
}
function cleanup(): void {
removeEventListeners([listener]);
clearTimeout(eventTimeout);
}
const result = await Promise.race([promise, abortPromise]).then(
(r) => {
cleanup();
return r;
},
(error) => {
cleanup();
throw error;
}
);
if (result instanceof Error) throw result;
return result;
}
function evaluationString(fun: Function | string, ...args: unknown[]): string {
if (isString(fun)) {
assert(args.length === 0, 'Cannot evaluate a string with arguments');
return fun;
}
function serializeArgument(arg: unknown): string {
if (Object.is(arg, undefined)) return 'undefined';
return JSON.stringify(arg);
}
return `(${fun})(${args.map(serializeArgument).join(',')})`;
}
async function waitWithTimeout<T extends any>(
promise: Promise<T>,
taskName: string,
timeout: number
): Promise<T> {
let reject;
const timeoutError = new TimeoutError(
`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`
);
const timeoutPromise = new Promise<T>((resolve, x) => (reject = x));
let timeoutTimer = null;
if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutTimer) clearTimeout(timeoutTimer);
}
}
async function readProtocolStream(
client: CDPSession,
handle: string,
path?: string
): Promise<Buffer> {
let eof = false;
let file;
if (path) file = await openAsync(path, 'w');
const bufs = [];
while (!eof) {
const response = await client.send('IO.read', { handle });
eof = response.eof;
const buf = Buffer.from(
response.data,
response.base64Encoded ? 'base64' : undefined
);
bufs.push(buf);
if (path) await writeAsync(file, buf);
}
if (path) await closeAsync(file);
await client.send('IO.close', { handle });
let resultBuffer = null;
try {
resultBuffer = Buffer.concat(bufs);
} finally {
return resultBuffer;
}
}
export const helper = {
promisify,
evaluationString,
readProtocolStream,
waitWithTimeout,
waitForEvent,
isString,
isNumber,
addEventListener,
removeEventListeners,
valueFromRemoteObject,
installAsyncStackHooks,
getExceptionMessage,
releaseObject,
};

Просмотреть файл

@ -0,0 +1,241 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as debug from 'debug';
import * as removeFolder from 'rimraf';
import * as childProcess from 'child_process';
import { helper, assert, debugError } from '../helper';
import type { LaunchOptions } from './LaunchOptions';
import { Connection } from '../Connection';
import { WebSocketTransport } from '../WebSocketTransport';
import { PipeTransport } from '../PipeTransport';
import * as readline from 'readline';
import { TimeoutError } from '../Errors';
const removeFolderAsync = helper.promisify(removeFolder);
const debugLauncher = debug('puppeteer:launcher');
export class BrowserRunner {
private _executablePath: string;
private _processArguments: string[];
private _tempDirectory?: string;
proc = null;
connection = null;
private _closed = true;
private _listeners = [];
private _processClosing: Promise<void>;
constructor(
executablePath: string,
processArguments: string[],
tempDirectory?: string
) {
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
}
start(options: LaunchOptions = {}): void {
const {
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
dumpio,
env,
pipe,
} = options;
let stdio: Array<'ignore' | 'pipe'> = ['pipe', 'pipe', 'pipe'];
if (pipe) {
if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
assert(!this.proc, 'This process has previously been started.');
debugLauncher(
`Calling ${this._executablePath} ${this._processArguments.join(' ')}`
);
this.proc = childProcess.spawn(
this._executablePath,
this._processArguments,
{
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio,
}
);
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill) => {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch((error) => console.error(error));
} else {
fulfill();
}
});
});
this._listeners = [
helper.addEventListener(process, 'exit', this.kill.bind(this)),
];
if (handleSIGINT)
this._listeners.push(
helper.addEventListener(process, 'SIGINT', () => {
this.kill();
process.exit(130);
})
);
if (handleSIGTERM)
this._listeners.push(
helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
);
if (handleSIGHUP)
this._listeners.push(
helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
);
}
close(): Promise<void> {
if (this._closed) return Promise.resolve();
helper.removeEventListeners(this._listeners);
if (this._tempDirectory) {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
this.connection.send('Browser.close').catch((error) => {
debugError(error);
this.kill();
});
}
return this._processClosing;
}
kill(): void {
helper.removeEventListeners(this._listeners);
if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
else process.kill(-this.proc.pid, 'SIGKILL');
} catch (error) {
// the process might have already stopped
}
}
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder.sync(this._tempDirectory);
} catch (error) {}
}
async setupConnection(options: {
usePipe?: boolean;
timeout: number;
slowMo: number;
preferredRevision: string;
}): Promise<Connection> {
const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(
this.proc,
timeout,
preferredRevision
);
const transport = await WebSocketTransport.create(browserWSEndpoint);
this.connection = new Connection(browserWSEndpoint, transport, slowMo);
} else {
// stdio was assigned during start(), and the 'pipe' option there adds the 4th and 5th items to stdio array
const { 3: pipeWrite, 4: pipeRead } = this.proc.stdio;
const transport = new PipeTransport(
pipeWrite as NodeJS.WritableStream,
pipeRead as NodeJS.ReadableStream
);
this.connection = new Connection('', transport, slowMo);
}
return this.connection;
}
}
function waitForWSEndpoint(
browserProcess: childProcess.ChildProcess,
timeout: number,
preferredRevision: string
): Promise<string> {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper.addEventListener(browserProcess, 'error', (error) =>
onClose(error)
),
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
/**
* @param {!Error=} error
*/
function onClose(error?: Error): void {
cleanup();
reject(
new Error(
[
'Failed to launch the browser process!' +
(error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')
)
);
}
function onTimeout(): void {
cleanup();
reject(
new TimeoutError(
`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`
)
);
}
function onLine(line: string): void {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) return;
cleanup();
resolve(match[1]);
}
function cleanup(): void {
if (timeoutId) clearTimeout(timeoutId);
helper.removeEventListeners(listeners);
}
});
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше