forked from mirrors/gecko-dev
Bug 1797723 - [puppeteer] Sync vendored puppeteer to v18.0.0. r=webdriver-reviewers,whimboo,jdescottes
Depends on D166650 Differential Revision: https://phabricator.services.mozilla.com/D166651
This commit is contained in:
parent
b306cf161f
commit
d115e24d80
109 changed files with 7883 additions and 4919 deletions
|
|
@ -126,6 +126,8 @@ _OPT\.OBJ/
|
||||||
^remote/test/puppeteer/test/build
|
^remote/test/puppeteer/test/build
|
||||||
^remote/test/puppeteer/test/output-firefox
|
^remote/test/puppeteer/test/output-firefox
|
||||||
^remote/test/puppeteer/test/output-chromium
|
^remote/test/puppeteer/test/output-chromium
|
||||||
|
^remote/test/puppeteer/testserver/lib/
|
||||||
|
^remote/test/puppeteer/utils/mochaRunner/lib/
|
||||||
^remote/test/puppeteer/website
|
^remote/test/puppeteer/website
|
||||||
|
|
||||||
# git checkout of libstagefright
|
# git checkout of libstagefright
|
||||||
|
|
|
||||||
2
remote/.gitignore
vendored
2
remote/.gitignore
vendored
|
|
@ -15,4 +15,6 @@ test/puppeteer/src/generated
|
||||||
test/puppeteer/test/build
|
test/puppeteer/test/build
|
||||||
test/puppeteer/test/output-firefox
|
test/puppeteer/test/output-firefox
|
||||||
test/puppeteer/test/output-chromium
|
test/puppeteer/test/output-chromium
|
||||||
|
test/puppeteer/testserver/lib/
|
||||||
|
test/puppeteer/utils/mochaRunner/lib/
|
||||||
test/puppeteer/website
|
test/puppeteer/website
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import mozprofile
|
||||||
from mach.decorators import Command, CommandArgument, SubCommand
|
from mach.decorators import Command, CommandArgument, SubCommand
|
||||||
from mozbuild import nodeutil
|
from mozbuild import nodeutil
|
||||||
from mozbuild.base import BinaryNotFoundException, MozbuildObject
|
from mozbuild.base import BinaryNotFoundException, MozbuildObject
|
||||||
from six import iteritems
|
|
||||||
|
|
||||||
EX_CONFIG = 78
|
EX_CONFIG = 78
|
||||||
EX_SOFTWARE = 70
|
EX_SOFTWARE = 70
|
||||||
|
|
@ -261,8 +260,10 @@ class MochaOutputHandler(object):
|
||||||
if not status and not test_start:
|
if not status and not test_start:
|
||||||
return
|
return
|
||||||
test_info = event[1]
|
test_info = event[1]
|
||||||
test_name = test_info.get("fullTitle", "")
|
test_full_title = test_info.get("fullTitle", "")
|
||||||
|
test_name = test_full_title
|
||||||
test_path = test_info.get("file", "")
|
test_path = test_info.get("file", "")
|
||||||
|
test_file_name = os.path.basename(test_path).replace(".js", "")
|
||||||
test_err = test_info.get("err")
|
test_err = test_info.get("err")
|
||||||
if status == "FAIL" and test_err:
|
if status == "FAIL" and test_err:
|
||||||
if "timeout" in test_err.lower():
|
if "timeout" in test_err.lower():
|
||||||
|
|
@ -276,7 +277,32 @@ class MochaOutputHandler(object):
|
||||||
if test_start:
|
if test_start:
|
||||||
self.logger.test_start(test_name)
|
self.logger.test_start(test_name)
|
||||||
return
|
return
|
||||||
expected = self.expected.get(test_name, ["PASS"])
|
expected_name = "[{}] {}".format(test_file_name, test_full_title)
|
||||||
|
expected_item = next(
|
||||||
|
(
|
||||||
|
expectation
|
||||||
|
for expectation in list(self.expected)
|
||||||
|
if expectation["testIdPattern"] == expected_name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if expected_item is None:
|
||||||
|
# if there is no expectation data for a specific test case,
|
||||||
|
# try to find data for a whole file.
|
||||||
|
expected_item_for_file = next(
|
||||||
|
(
|
||||||
|
expectation
|
||||||
|
for expectation in list(self.expected)
|
||||||
|
if expectation["testIdPattern"] == f"[{test_file_name}]"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if expected_item_for_file is None:
|
||||||
|
expected = ["PASS"]
|
||||||
|
else:
|
||||||
|
expected = expected_item_for_file["expectations"]
|
||||||
|
else:
|
||||||
|
expected = expected_item["expectations"]
|
||||||
# mozlog doesn't really allow unexpected skip,
|
# mozlog doesn't really allow unexpected skip,
|
||||||
# so if a test is disabled just expect that and note the unexpected skip
|
# so if a test is disabled just expect that and note the unexpected skip
|
||||||
# Also, mocha doesn't log test-start for skipped tests
|
# Also, mocha doesn't log test-start for skipped tests
|
||||||
|
|
@ -308,34 +334,7 @@ class MochaOutputHandler(object):
|
||||||
known_intermittent=known_intermittent,
|
known_intermittent=known_intermittent,
|
||||||
)
|
)
|
||||||
|
|
||||||
def new_expected(self):
|
def after_end(self):
|
||||||
new_expected = OrderedDict()
|
|
||||||
for test_name, status in iteritems(self.test_results):
|
|
||||||
if test_name not in self.expected:
|
|
||||||
new_status = [status]
|
|
||||||
else:
|
|
||||||
if status in self.expected[test_name]:
|
|
||||||
new_status = self.expected[test_name]
|
|
||||||
else:
|
|
||||||
new_status = [status]
|
|
||||||
new_expected[test_name] = new_status
|
|
||||||
return new_expected
|
|
||||||
|
|
||||||
def after_end(self, subset=False):
|
|
||||||
if not subset:
|
|
||||||
missing = set(self.expected) - set(self.test_results)
|
|
||||||
extra = set(self.test_results) - set(self.expected)
|
|
||||||
if missing:
|
|
||||||
self.has_unexpected = True
|
|
||||||
for test_name in missing:
|
|
||||||
self.logger.error("TEST-UNEXPECTED-MISSING %s" % (test_name,))
|
|
||||||
if self.expected and extra:
|
|
||||||
self.has_unexpected = True
|
|
||||||
for test_name in extra:
|
|
||||||
self.logger.error(
|
|
||||||
"TEST-UNEXPECTED-MISSING Unknown new test %s" % (test_name,)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.unexpected_skips:
|
if self.unexpected_skips:
|
||||||
self.has_unexpected = True
|
self.has_unexpected = True
|
||||||
for test_name in self.unexpected_skips:
|
for test_name in self.unexpected_skips:
|
||||||
|
|
@ -392,8 +391,6 @@ class PuppeteerRunner(MozbuildObject):
|
||||||
before invoking npm. Overrides default preferences.
|
before invoking npm. Overrides default preferences.
|
||||||
`enable_webrender`:
|
`enable_webrender`:
|
||||||
Boolean to indicate whether to enable WebRender compositor in Gecko.
|
Boolean to indicate whether to enable WebRender compositor in Gecko.
|
||||||
`write_results`:
|
|
||||||
Path to write the results json file
|
|
||||||
`subset`
|
`subset`
|
||||||
Indicates only a subset of tests are being run, so we should
|
Indicates only a subset of tests are being run, so we should
|
||||||
skip the check for missing results
|
skip the check for missing results
|
||||||
|
|
@ -425,6 +422,7 @@ class PuppeteerRunner(MozbuildObject):
|
||||||
"--timeout",
|
"--timeout",
|
||||||
"20000",
|
"20000",
|
||||||
"--no-parallel",
|
"--no-parallel",
|
||||||
|
"--no-coverage",
|
||||||
]
|
]
|
||||||
env["HEADLESS"] = str(params.get("headless", False))
|
env["HEADLESS"] = str(params.get("headless", False))
|
||||||
|
|
||||||
|
|
@ -454,15 +452,31 @@ class PuppeteerRunner(MozbuildObject):
|
||||||
env["EXTRA_LAUNCH_OPTIONS"] = json.dumps(extra_options)
|
env["EXTRA_LAUNCH_OPTIONS"] = json.dumps(extra_options)
|
||||||
|
|
||||||
expected_path = os.path.join(
|
expected_path = os.path.join(
|
||||||
os.path.dirname(__file__), "test", "puppeteer-expected.json"
|
os.path.dirname(__file__),
|
||||||
|
"test",
|
||||||
|
"puppeteer",
|
||||||
|
"test",
|
||||||
|
"TestExpectations.json",
|
||||||
)
|
)
|
||||||
if product == "firefox" and os.path.exists(expected_path):
|
if os.path.exists(expected_path):
|
||||||
with open(expected_path) as f:
|
with open(expected_path) as f:
|
||||||
expected_data = json.load(f)
|
expected_data = json.load(f)
|
||||||
else:
|
else:
|
||||||
expected_data = {}
|
expected_data = []
|
||||||
|
# Filter expectation data for the selected browser,
|
||||||
|
# headless or headful mode, and the operating system.
|
||||||
|
platform = os.uname().sysname.lower() if os.uname() else "win32"
|
||||||
|
expectations = filter(
|
||||||
|
lambda el: product in el["parameters"]
|
||||||
|
and (
|
||||||
|
(env["HEADLESS"] == "False" and "headless" not in el["parameters"])
|
||||||
|
or "headful" not in el["parameters"]
|
||||||
|
)
|
||||||
|
and platform in el["platforms"],
|
||||||
|
expected_data,
|
||||||
|
)
|
||||||
|
|
||||||
output_handler = MochaOutputHandler(logger, expected_data)
|
output_handler = MochaOutputHandler(logger, list(expectations))
|
||||||
proc = npm(
|
proc = npm(
|
||||||
*command,
|
*command,
|
||||||
cwd=self.puppeteer_dir,
|
cwd=self.puppeteer_dir,
|
||||||
|
|
@ -476,7 +490,7 @@ class PuppeteerRunner(MozbuildObject):
|
||||||
# failure, so use an output_timeout as a fallback
|
# failure, so use an output_timeout as a fallback
|
||||||
wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False)
|
wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False)
|
||||||
|
|
||||||
output_handler.after_end(params.get("subset", False))
|
output_handler.after_end()
|
||||||
|
|
||||||
# Non-zero return codes are non-fatal for now since we have some
|
# Non-zero return codes are non-fatal for now since we have some
|
||||||
# issues with unresolved promises that shouldn't otherwise block
|
# issues with unresolved promises that shouldn't otherwise block
|
||||||
|
|
@ -484,12 +498,6 @@ class PuppeteerRunner(MozbuildObject):
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
logger.warning("npm exited with code %s" % proc.returncode)
|
logger.warning("npm exited with code %s" % proc.returncode)
|
||||||
|
|
||||||
if params["write_results"]:
|
|
||||||
with open(params["write_results"], "w") as f:
|
|
||||||
json.dump(
|
|
||||||
output_handler.new_expected(), f, indent=2, separators=(",", ": ")
|
|
||||||
)
|
|
||||||
|
|
||||||
if output_handler.has_unexpected:
|
if output_handler.has_unexpected:
|
||||||
exit(1, "Got unexpected results")
|
exit(1, "Got unexpected results")
|
||||||
|
|
||||||
|
|
@ -547,18 +555,6 @@ def create_parser_puppeteer():
|
||||||
"debug level messages with -v, trace messages with -vv,"
|
"debug level messages with -v, trace messages with -vv,"
|
||||||
"and to not truncate long trace messages with -vvv",
|
"and to not truncate long trace messages with -vvv",
|
||||||
)
|
)
|
||||||
p.add_argument(
|
|
||||||
"--write-results",
|
|
||||||
action="store",
|
|
||||||
nargs="?",
|
|
||||||
default=None,
|
|
||||||
const=os.path.join(
|
|
||||||
os.path.dirname(__file__), "test", "puppeteer-expected.json"
|
|
||||||
),
|
|
||||||
help="Path to write updated results to (defaults to the "
|
|
||||||
"expectations file if the argument is provided but "
|
|
||||||
"no path is passed)",
|
|
||||||
)
|
|
||||||
p.add_argument(
|
p.add_argument(
|
||||||
"--subset",
|
"--subset",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -597,7 +593,6 @@ def puppeteer_test(
|
||||||
verbosity=0,
|
verbosity=0,
|
||||||
tests=None,
|
tests=None,
|
||||||
product="firefox",
|
product="firefox",
|
||||||
write_results=None,
|
|
||||||
subset=False,
|
subset=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|
@ -657,7 +652,6 @@ def puppeteer_test(
|
||||||
"extra_prefs": prefs,
|
"extra_prefs": prefs,
|
||||||
"product": product,
|
"product": product,
|
||||||
"extra_launcher_options": options,
|
"extra_launcher_options": options,
|
||||||
"write_results": write_results,
|
|
||||||
"subset": subset,
|
"subset": subset,
|
||||||
}
|
}
|
||||||
puppeteer = command_context._spawn(PuppeteerRunner)
|
puppeteer = command_context._spawn(PuppeteerRunner)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -18,10 +18,10 @@ module.exports = {
|
||||||
reporter: 'dot',
|
reporter: 'dot',
|
||||||
logLevel: 'debug',
|
logLevel: 'debug',
|
||||||
require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
|
require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
|
||||||
spec: 'test/build/*.spec.js',
|
spec: 'test/build/**/*.spec.js',
|
||||||
exit: !!process.env.CI,
|
exit: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
parallel: !!process.env.PARALLEL,
|
parallel: !!process.env.PARALLEL,
|
||||||
timeout: 25 * 1000,
|
timeout: 25_000,
|
||||||
reporter: process.env.CI ? 'spec' : 'dot',
|
reporter: process.env.CI ? 'spec' : 'dot',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
".": "17.1.2"
|
".": "18.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,32 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* fix bounding box visibility conditions (#8954)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53))
|
||||||
|
* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8))
|
||||||
|
* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7))
|
||||||
|
|
||||||
|
## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919)
|
||||||
|
* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915)
|
||||||
|
|
||||||
## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)
|
## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ to the project.
|
||||||
|
|
||||||
function JSONExtra(runner, options) {
|
function JSONExtra(runner, options) {
|
||||||
mocha.reporters.Base.call(this, runner, options);
|
mocha.reporters.Base.call(this, runner, options);
|
||||||
|
mocha.reporters.JSON.call(this, runner, options);
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
runner.once(constants.EVENT_RUN_BEGIN, function () {
|
runner.once(constants.EVENT_RUN_BEGIN, function () {
|
||||||
writeEvent(['start', { total: runner.total }]);
|
writeEvent(['start', {total: runner.total}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.on(constants.EVENT_TEST_PASS, function (test) {
|
runner.on(constants.EVENT_TEST_PASS, function (test) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
schema: 1
|
|
||||||
bugzilla:
|
bugzilla:
|
||||||
component: Agent
|
component: Agent
|
||||||
product: Remote Protocol
|
product: Remote Protocol
|
||||||
|
|
@ -6,5 +5,6 @@ origin:
|
||||||
description: Headless Chrome Node API
|
description: Headless Chrome Node API
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
name: puppeteer
|
name: puppeteer
|
||||||
release: 0d2d99efeca73fba255fb10b28b5d3f50c2e20e4
|
release: 7d6927209e5d557891bd618ddb01d54bc3566307
|
||||||
url: https://github.com/puppeteer/puppeteer
|
url: /Users/alexandraborovova/Projects/puppeteer
|
||||||
|
schema: 1
|
||||||
|
|
|
||||||
22
remote/test/puppeteer/package-lock.json
generated
22
remote/test/puppeteer/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "puppeteer",
|
"name": "puppeteer",
|
||||||
"version": "17.1.2",
|
"version": "18.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "puppeteer",
|
"name": "puppeteer",
|
||||||
"version": "17.1.2",
|
"version": "18.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -80,7 +80,8 @@
|
||||||
"text-diff": "1.0.1",
|
"text-diff": "1.0.1",
|
||||||
"tsd": "0.22.0",
|
"tsd": "0.22.0",
|
||||||
"tsx": "3.8.2",
|
"tsx": "3.8.2",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4",
|
||||||
|
"zod": "3.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.1.0"
|
"node": ">=14.1.0"
|
||||||
|
|
@ -7809,6 +7810,15 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"commander": "^2.20.3"
|
"commander": "^2.20.3"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
|
||||||
|
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -13501,6 +13511,12 @@
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"validator": "^13.7.0"
|
"validator": "^13.7.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"version": "3.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
|
||||||
|
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "puppeteer",
|
"name": "puppeteer",
|
||||||
"version": "17.1.2",
|
"version": "18.0.0",
|
||||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"puppeteer",
|
"puppeteer",
|
||||||
|
|
@ -27,14 +27,14 @@
|
||||||
"node": ">=14.1.0"
|
"node": ">=14.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "c8 --check-coverage --lines 93 run-s test:chrome:* test:firefox",
|
"test": "cross-env MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node utils/mochaRunner/lib/main.js",
|
||||||
"test:types": "tsd",
|
"test:types": "tsd",
|
||||||
"test:install": "scripts/test-install.sh",
|
"test:install": "scripts/test-install.sh",
|
||||||
"test:firefox": "cross-env PUPPETEER_PRODUCT=firefox MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
"test:firefox": "npm run test -- --test-suite firefox-headless",
|
||||||
"test:chrome": "run-s test:chrome:*",
|
"test:chrome": "run-s test:chrome:*",
|
||||||
"test:chrome:headless": "cross-env HEADLESS=true PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
"test:chrome:headless": "npm run test -- --test-suite chrome-headless",
|
||||||
"test:chrome:headless-chrome": "cross-env HEADLESS=chrome PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
"test:chrome:headless-chrome": "npm run test -- --test-suite chrome-new-headless",
|
||||||
"test:chrome:headful": "cross-env HEADLESS=false PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
"test:chrome:headful": "npm run test -- --test-suite chrome-headful",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"prepare": "node typescript-if-required.js && husky install",
|
"prepare": "node typescript-if-required.js && husky install",
|
||||||
"lint": "run-s lint:prettier lint:eslint",
|
"lint": "run-s lint:prettier lint:eslint",
|
||||||
|
|
@ -139,6 +139,7 @@
|
||||||
"text-diff": "1.0.1",
|
"text-diff": "1.0.1",
|
||||||
"tsd": "0.22.0",
|
"tsd": "0.22.0",
|
||||||
"tsx": "3.8.2",
|
"tsx": "3.8.2",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4",
|
||||||
|
"zod": "3.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,31 @@ echo '{"type":"module"}' >>$TMPDIR/package.json
|
||||||
npm install --loglevel silent "${tarball}"
|
npm install --loglevel silent "${tarball}"
|
||||||
node --input-type="module" --eval="import puppeteer from 'puppeteer-core'"
|
node --input-type="module" --eval="import puppeteer from 'puppeteer-core'"
|
||||||
node --input-type="module" --eval="import 'puppeteer-core/lib/esm/puppeteer/revisions.js';"
|
node --input-type="module" --eval="import 'puppeteer-core/lib/esm/puppeteer/revisions.js';"
|
||||||
|
|
||||||
|
echo "Testing... Puppeteer Core launch with executablePath"
|
||||||
|
TMPDIR="$(mktemp -d)"
|
||||||
|
cd "$TMPDIR"
|
||||||
|
echo '{"type":"module"}' >> "$TMPDIR/package.json"
|
||||||
|
npm install --loglevel silent "${tarball}"
|
||||||
|
# The test tries to launch the node process because
|
||||||
|
# real browsers are not downloaded by puppeteer-core.
|
||||||
|
# The expected error is "Failed to launch the browser process"
|
||||||
|
# so the test verifies that it does not fail for other reasons.
|
||||||
|
node --input-type="module" --eval="
|
||||||
|
import puppeteer from 'puppeteer-core';
|
||||||
|
(async () => {
|
||||||
|
puppeteer.launch({
|
||||||
|
product: 'firefox',
|
||||||
|
executablePath: 'node'
|
||||||
|
}).catch(error => error.message.includes('Failed to launch the browser process') ? process.exit(0) : process.exit(1));
|
||||||
|
})();
|
||||||
|
"
|
||||||
|
node --input-type="module" --eval="
|
||||||
|
import puppeteer from 'puppeteer-core';
|
||||||
|
(async () => {
|
||||||
|
puppeteer.launch({
|
||||||
|
product: 'chrome',
|
||||||
|
executablePath: 'node'
|
||||||
|
}).catch(error => error.message.includes('Failed to launch the browser process') ? process.exit(0) : process.exit(1));
|
||||||
|
})();
|
||||||
|
"
|
||||||
|
|
|
||||||
628
remote/test/puppeteer/src/api/Browser.ts
Normal file
628
remote/test/puppeteer/src/api/Browser.ts
Normal file
|
|
@ -0,0 +1,628 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
import {ChildProcess} from 'child_process';
|
||||||
|
import {Protocol} from 'devtools-protocol';
|
||||||
|
import {EventEmitter} from '../common/EventEmitter.js';
|
||||||
|
import type {Page} from '../common/Page.js'; // TODO: move to ./api
|
||||||
|
import type {Target} from '../common/Target.js'; // TODO: move to ./api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowserContext options.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface BrowserContextOptions {
|
||||||
|
/**
|
||||||
|
* Proxy server with optional port to use for all requests.
|
||||||
|
* Username and password can be set in `Page.authenticate`.
|
||||||
|
*/
|
||||||
|
proxyServer?: string;
|
||||||
|
/**
|
||||||
|
* Bypass the proxy for the given list of hosts.
|
||||||
|
*/
|
||||||
|
proxyBypassList?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type BrowserCloseCallback = () => Promise<void> | void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type TargetFilterCallback = (
|
||||||
|
target: Protocol.Target.TargetInfo
|
||||||
|
) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type IsPageTargetCallback = (
|
||||||
|
target: Protocol.Target.TargetInfo
|
||||||
|
) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
|
||||||
|
Permission,
|
||||||
|
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'],
|
||||||
|
['persistent-storage', 'durableStorage'],
|
||||||
|
['idle-detection', 'idleDetection'],
|
||||||
|
// chrome-specific permissions we have.
|
||||||
|
['midi-sysex', 'midiSysex'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type Permission =
|
||||||
|
| 'geolocation'
|
||||||
|
| 'midi'
|
||||||
|
| 'notifications'
|
||||||
|
| 'camera'
|
||||||
|
| 'microphone'
|
||||||
|
| 'background-sync'
|
||||||
|
| 'ambient-light-sensor'
|
||||||
|
| 'accelerometer'
|
||||||
|
| 'gyroscope'
|
||||||
|
| 'magnetometer'
|
||||||
|
| 'accessibility-events'
|
||||||
|
| 'clipboard-read'
|
||||||
|
| 'clipboard-write'
|
||||||
|
| 'payment-handler'
|
||||||
|
| 'persistent-storage'
|
||||||
|
| 'idle-detection'
|
||||||
|
| 'midi-sysex';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface WaitForTargetOptions {
|
||||||
|
/**
|
||||||
|
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
|
||||||
|
* @defaultValue 30 seconds.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All the events a {@link Browser | browser instance} may emit.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const enum BrowserEmittedEvents {
|
||||||
|
/**
|
||||||
|
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
|
||||||
|
* might happen because of one of the following:
|
||||||
|
*
|
||||||
|
* - Chromium is closed or crashed
|
||||||
|
*
|
||||||
|
* - The {@link Browser.disconnect | browser.disconnect } method was called.
|
||||||
|
*/
|
||||||
|
Disconnected = 'disconnected',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the url of a target changes. Contains a {@link Target} instance.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Note that this includes target changes in incognito browser contexts.
|
||||||
|
*/
|
||||||
|
TargetChanged = 'targetchanged',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when a target is created, for example when a new page is opened by
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
|
||||||
|
* or by {@link Browser.newPage | browser.newPage}
|
||||||
|
*
|
||||||
|
* Contains a {@link Target} instance.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Note that this includes target creations in incognito browser contexts.
|
||||||
|
*/
|
||||||
|
TargetCreated = 'targetcreated',
|
||||||
|
/**
|
||||||
|
* Emitted when a target is destroyed, for example when a page is closed.
|
||||||
|
* Contains a {@link Target} instance.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Note that this includes target destructions in incognito browser contexts.
|
||||||
|
*/
|
||||||
|
TargetDestroyed = 'targetdestroyed',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Browser is created when Puppeteer connects to a Chromium instance, either through
|
||||||
|
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
|
||||||
|
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* An example of using a {@link Browser} to create a {@link Page}:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const puppeteer = require('puppeteer');
|
||||||
|
*
|
||||||
|
* (async () => {
|
||||||
|
* const browser = await puppeteer.launch();
|
||||||
|
* const page = await browser.newPage();
|
||||||
|
* await page.goto('https://example.com');
|
||||||
|
* await browser.close();
|
||||||
|
* })();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* An example of disconnecting from and reconnecting to a {@link Browser}:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const puppeteer = require('puppeteer');
|
||||||
|
*
|
||||||
|
* (async () => {
|
||||||
|
* const browser = await puppeteer.launch();
|
||||||
|
* // Store the endpoint to be able to reconnect to Chromium
|
||||||
|
* const browserWSEndpoint = browser.wsEndpoint();
|
||||||
|
* // Disconnect puppeteer from Chromium
|
||||||
|
* browser.disconnect();
|
||||||
|
*
|
||||||
|
* // Use the endpoint to reestablish a connection
|
||||||
|
* const browser2 = await puppeteer.connect({browserWSEndpoint});
|
||||||
|
* // Close Chromium
|
||||||
|
* await browser2.close();
|
||||||
|
* })();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class Browser extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_attach(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_detach(): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
get _targets(): Map<string, Target> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The spawned browser process. Returns `null` if the browser instance was created with
|
||||||
|
* {@link Puppeteer.connect}.
|
||||||
|
*/
|
||||||
|
process(): ChildProcess | null {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new incognito browser context. This won't share cookies/cache with other
|
||||||
|
* browser contexts.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* (async () => {
|
||||||
|
* const browser = await puppeteer.launch();
|
||||||
|
* // Create a new incognito browser context.
|
||||||
|
* const context = await browser.createIncognitoBrowserContext();
|
||||||
|
* // Create a new page in a pristine context.
|
||||||
|
* const page = await context.newPage();
|
||||||
|
* // Do stuff
|
||||||
|
* await page.goto('https://example.com');
|
||||||
|
* })();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
createIncognitoBrowserContext(
|
||||||
|
options?: BrowserContextOptions
|
||||||
|
): Promise<BrowserContext>;
|
||||||
|
createIncognitoBrowserContext(): Promise<BrowserContext> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of all open browser contexts. In a newly created browser, this will
|
||||||
|
* return a single instance of {@link BrowserContext}.
|
||||||
|
*/
|
||||||
|
browserContexts(): BrowserContext[] {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default browser context. The default browser context cannot be closed.
|
||||||
|
*/
|
||||||
|
defaultBrowserContext(): BrowserContext {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_disposeContext(contextId?: string): Promise<void>;
|
||||||
|
_disposeContext(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser websocket endpoint which can be used as an argument to
|
||||||
|
* {@link Puppeteer.connect}.
|
||||||
|
*
|
||||||
|
* @returns The Browser websocket url.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* The format is `ws://${host}:${port}/devtools/browser/<id>`.
|
||||||
|
*
|
||||||
|
* You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`.
|
||||||
|
* Learn more about the
|
||||||
|
* {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and
|
||||||
|
* the {@link
|
||||||
|
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
|
||||||
|
* | browser endpoint}.
|
||||||
|
*/
|
||||||
|
wsEndpoint(): string {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise which resolves to a new {@link Page} object. The Page is created in
|
||||||
|
* a default browser context.
|
||||||
|
*/
|
||||||
|
newPage(): Promise<Page> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_createPageInContext(contextId?: string): Promise<Page>;
|
||||||
|
_createPageInContext(): Promise<Page> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All active targets inside the Browser. In case of multiple browser contexts, returns
|
||||||
|
* an array with all the targets in all browser contexts.
|
||||||
|
*/
|
||||||
|
targets(): Target[] {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target associated with the browser.
|
||||||
|
*/
|
||||||
|
target(): Target {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a target in all browser contexts.
|
||||||
|
*
|
||||||
|
* @param predicate - A function to be run for every target.
|
||||||
|
* @returns The first target found that matches the `predicate` function.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* An example of finding a target for a page opened via `window.open`:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* await page.evaluate(() => window.open('https://www.example.com/'));
|
||||||
|
* const newWindowTarget = await browser.waitForTarget(
|
||||||
|
* target => target.url() === 'https://www.example.com/'
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
waitForTarget(
|
||||||
|
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||||
|
options?: WaitForTargetOptions
|
||||||
|
): Promise<Target>;
|
||||||
|
waitForTarget(): Promise<Target> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of all open pages inside the Browser.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* In case of multiple browser contexts, returns an array with all the pages in all
|
||||||
|
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
|
||||||
|
* here. You can find them using {@link Target.page}.
|
||||||
|
*/
|
||||||
|
pages(): Promise<Page[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the browser name and version.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For
|
||||||
|
* non-headless, this is similar to `Chrome/61.0.3153.0`.
|
||||||
|
*
|
||||||
|
* The format of browser.version() might change with future releases of Chromium.
|
||||||
|
*/
|
||||||
|
version(): Promise<string> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser's original user agent. Pages can override the browser user agent with
|
||||||
|
* {@link Page.setUserAgent}.
|
||||||
|
*/
|
||||||
|
userAgent(): Promise<string> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object
|
||||||
|
* itself is considered to be disposed and cannot be used anymore.
|
||||||
|
*/
|
||||||
|
close(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects Puppeteer from the browser, but leaves the Chromium process running.
|
||||||
|
* After calling `disconnect`, the {@link Browser} object is considered disposed and
|
||||||
|
* cannot be used anymore.
|
||||||
|
*/
|
||||||
|
disconnect(): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the browser is connected.
|
||||||
|
*/
|
||||||
|
isConnected(): boolean {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const enum BrowserContextEmittedEvents {
|
||||||
|
/**
|
||||||
|
* Emitted when the url of a target inside the browser context changes.
|
||||||
|
* Contains a {@link Target} instance.
|
||||||
|
*/
|
||||||
|
TargetChanged = 'targetchanged',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when a target is created within the browser context, for example
|
||||||
|
* when a new page is opened by
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
|
||||||
|
* or by {@link BrowserContext.newPage | browserContext.newPage}
|
||||||
|
*
|
||||||
|
* Contains a {@link Target} instance.
|
||||||
|
*/
|
||||||
|
TargetCreated = 'targetcreated',
|
||||||
|
/**
|
||||||
|
* Emitted when a target is destroyed within the browser context, for example
|
||||||
|
* when a page is closed. Contains a {@link Target} instance.
|
||||||
|
*/
|
||||||
|
TargetDestroyed = 'targetdestroyed',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowserContexts provide a way to operate multiple independent browser
|
||||||
|
* sessions. When a browser is launched, it has a single BrowserContext used by
|
||||||
|
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
|
||||||
|
* in the default browser context.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
|
||||||
|
* will emit various events which are documented in the
|
||||||
|
* {@link BrowserContextEmittedEvents} enum.
|
||||||
|
*
|
||||||
|
* If a page opens another page, e.g. with a `window.open` call, the popup will
|
||||||
|
* belong to the parent page's browser context.
|
||||||
|
*
|
||||||
|
* Puppeteer allows creation of "incognito" browser contexts with
|
||||||
|
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
|
||||||
|
* method. "Incognito" browser contexts don't write any browsing data to disk.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* // Create a new incognito browser context
|
||||||
|
* const context = await browser.createIncognitoBrowserContext();
|
||||||
|
* // Create a new page inside context.
|
||||||
|
* const page = await context.newPage();
|
||||||
|
* // ... do stuff with page ...
|
||||||
|
* await page.goto('https://example.com');
|
||||||
|
* // Dispose context once it's no longer needed.
|
||||||
|
* await context.close();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class BrowserContext extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of all active targets inside the browser context.
|
||||||
|
*/
|
||||||
|
targets(): Target[] {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This searches for a target in this specific browser context.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* An example of finding a target for a page opened via `window.open`:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* await page.evaluate(() => window.open('https://www.example.com/'));
|
||||||
|
* const newWindowTarget = await browserContext.waitForTarget(
|
||||||
|
* target => target.url() === 'https://www.example.com/'
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param predicate - A function to be run for every target
|
||||||
|
* @param options - An object of options. Accepts a timout,
|
||||||
|
* which is the maximum wait time in milliseconds.
|
||||||
|
* Pass `0` to disable the timeout. Defaults to 30 seconds.
|
||||||
|
* @returns Promise which resolves to the first target found
|
||||||
|
* that matches the `predicate` function.
|
||||||
|
*/
|
||||||
|
waitForTarget(
|
||||||
|
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||||
|
options?: {timeout?: number}
|
||||||
|
): Promise<Target>;
|
||||||
|
waitForTarget(): Promise<Target> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of all pages inside the browser context.
|
||||||
|
*
|
||||||
|
* @returns Promise which resolves to an array of all open pages.
|
||||||
|
* Non visible pages, such as `"background_page"`, will not be listed here.
|
||||||
|
* You can find them using {@link Target.page | the target page}.
|
||||||
|
*/
|
||||||
|
pages(): Promise<Page[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether BrowserContext is incognito.
|
||||||
|
* The default browser context is the only non-incognito browser context.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The default browser context cannot be closed.
|
||||||
|
*/
|
||||||
|
isIncognito(): boolean {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const context = browser.defaultBrowserContext();
|
||||||
|
* await context.overridePermissions('https://html5demos.com', [
|
||||||
|
* 'geolocation',
|
||||||
|
* ]);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
|
||||||
|
* @param permissions - An array of permissions to grant.
|
||||||
|
* All permissions that are not listed here will be automatically denied.
|
||||||
|
*/
|
||||||
|
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
|
||||||
|
overridePermissions(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all permission overrides for the browser context.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const context = browser.defaultBrowserContext();
|
||||||
|
* context.overridePermissions('https://example.com', ['clipboard-read']);
|
||||||
|
* // do stuff ..
|
||||||
|
* context.clearPermissionOverrides();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
clearPermissionOverrides(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new page in the browser context.
|
||||||
|
*/
|
||||||
|
newPage(): Promise<Page> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser this browser context belongs to.
|
||||||
|
*/
|
||||||
|
browser(): Browser {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the browser context. All the targets that belong to the browser context
|
||||||
|
* will be closed.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Only incognito browser contexts can be closed.
|
||||||
|
*/
|
||||||
|
close(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,8 @@ import {assert} from '../util/assert.js';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||||
import {InternalQueryHandler} from './QueryHandler.js';
|
import {PuppeteerQueryHandler} from './QueryHandler.js';
|
||||||
|
|
||||||
async function queryAXTree(
|
async function queryAXTree(
|
||||||
client: CDPSession,
|
client: CDPSession,
|
||||||
|
|
@ -95,7 +95,7 @@ const queryOneId = async (element: ElementHandle<Node>, selector: string) => {
|
||||||
return res[0].backendDOMNodeId;
|
return res[0].backendDOMNodeId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryOne: InternalQueryHandler['queryOne'] = async (
|
const queryOne: PuppeteerQueryHandler['queryOne'] = async (
|
||||||
element,
|
element,
|
||||||
selector
|
selector
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -108,7 +108,7 @@ const queryOne: InternalQueryHandler['queryOne'] = async (
|
||||||
)) as ElementHandle<Node>;
|
)) as ElementHandle<Node>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitFor: InternalQueryHandler['waitFor'] = async (
|
const waitFor: PuppeteerQueryHandler['waitFor'] = async (
|
||||||
elementOrFrame,
|
elementOrFrame,
|
||||||
selector,
|
selector,
|
||||||
options
|
options
|
||||||
|
|
@ -121,21 +121,20 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||||
frame = elementOrFrame.frame;
|
frame = elementOrFrame.frame;
|
||||||
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
||||||
}
|
}
|
||||||
const binding: PageBinding = {
|
|
||||||
name: 'ariaQuerySelector',
|
const ariaQuerySelector = async (selector: string) => {
|
||||||
pptrFunction: async (selector: string) => {
|
const id = await queryOneId(
|
||||||
const id = await queryOneId(
|
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
||||||
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
selector
|
||||||
selector
|
);
|
||||||
);
|
if (!id) {
|
||||||
if (!id) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
||||||
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
id
|
||||||
id
|
)) as ElementHandle<Node>;
|
||||||
)) as ElementHandle<Node>;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
||||||
(_: Element, selector: string) => {
|
(_: Element, selector: string) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -147,22 +146,19 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||||
element,
|
element,
|
||||||
selector,
|
selector,
|
||||||
options,
|
options,
|
||||||
binding
|
new Set([ariaQuerySelector])
|
||||||
);
|
);
|
||||||
if (element) {
|
if (element) {
|
||||||
await element.dispose();
|
await element.dispose();
|
||||||
}
|
}
|
||||||
if (!result) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!(result instanceof ElementHandle)) {
|
if (!(result instanceof ElementHandle)) {
|
||||||
await result.dispose();
|
await result?.dispose();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryAll: InternalQueryHandler['queryAll'] = async (
|
const queryAll: PuppeteerQueryHandler['queryAll'] = async (
|
||||||
element,
|
element,
|
||||||
selector
|
selector
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -182,7 +178,7 @@ const queryAll: InternalQueryHandler['queryAll'] = async (
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const ariaHandler: InternalQueryHandler = {
|
export const ariaHandler: PuppeteerQueryHandler = {
|
||||||
queryOne,
|
queryOne,
|
||||||
waitFor,
|
waitFor,
|
||||||
queryAll,
|
queryAll,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {ChildProcess} from 'child_process';
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
|
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
|
||||||
import {waitWithTimeout} from './util.js';
|
import {waitWithTimeout} from './util.js';
|
||||||
import {Page} from './Page.js';
|
import {Page} from './Page.js';
|
||||||
import {Viewport} from './PuppeteerViewport.js';
|
import {Viewport} from './PuppeteerViewport.js';
|
||||||
|
|
@ -27,196 +26,24 @@ import {TaskQueue} from './TaskQueue.js';
|
||||||
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
|
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
|
||||||
import {ChromeTargetManager} from './ChromeTargetManager.js';
|
import {ChromeTargetManager} from './ChromeTargetManager.js';
|
||||||
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
|
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
|
||||||
|
import {
|
||||||
/**
|
Browser as BrowserBase,
|
||||||
* BrowserContext options.
|
BrowserContext,
|
||||||
*
|
BrowserCloseCallback,
|
||||||
* @public
|
TargetFilterCallback,
|
||||||
*/
|
IsPageTargetCallback,
|
||||||
export interface BrowserContextOptions {
|
BrowserEmittedEvents,
|
||||||
/**
|
BrowserContextEmittedEvents,
|
||||||
* Proxy server with optional port to use for all requests.
|
BrowserContextOptions,
|
||||||
* Username and password can be set in `Page.authenticate`.
|
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
||||||
*/
|
WaitForTargetOptions,
|
||||||
proxyServer?: string;
|
|
||||||
/**
|
|
||||||
* Bypass the proxy for the given semi-colon-separated list of hosts.
|
|
||||||
*/
|
|
||||||
proxyBypassList?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export type BrowserCloseCallback = () => Promise<void> | void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type TargetFilterCallback = (
|
|
||||||
target: Protocol.Target.TargetInfo
|
|
||||||
) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export type IsPageTargetCallback = (
|
|
||||||
target: Protocol.Target.TargetInfo
|
|
||||||
) => boolean;
|
|
||||||
|
|
||||||
const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
|
|
||||||
Permission,
|
Permission,
|
||||||
Protocol.Browser.PermissionType
|
} from '../api/Browser.js';
|
||||||
>([
|
|
||||||
['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'],
|
|
||||||
['persistent-storage', 'durableStorage'],
|
|
||||||
['idle-detection', 'idleDetection'],
|
|
||||||
// chrome-specific permissions we have.
|
|
||||||
['midi-sysex', 'midiSysex'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @internal
|
||||||
*/
|
*/
|
||||||
export type Permission =
|
export class CDPBrowser extends BrowserBase {
|
||||||
| 'geolocation'
|
|
||||||
| 'midi'
|
|
||||||
| 'notifications'
|
|
||||||
| 'camera'
|
|
||||||
| 'microphone'
|
|
||||||
| 'background-sync'
|
|
||||||
| 'ambient-light-sensor'
|
|
||||||
| 'accelerometer'
|
|
||||||
| 'gyroscope'
|
|
||||||
| 'magnetometer'
|
|
||||||
| 'accessibility-events'
|
|
||||||
| 'clipboard-read'
|
|
||||||
| 'clipboard-write'
|
|
||||||
| 'payment-handler'
|
|
||||||
| 'persistent-storage'
|
|
||||||
| 'idle-detection'
|
|
||||||
| 'midi-sysex';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export interface WaitForTargetOptions {
|
|
||||||
/**
|
|
||||||
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
|
|
||||||
* @defaultValue 30 seconds.
|
|
||||||
*/
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All the events a {@link Browser | browser instance} may emit.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const enum BrowserEmittedEvents {
|
|
||||||
/**
|
|
||||||
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
|
|
||||||
* might happen because of one of the following:
|
|
||||||
*
|
|
||||||
* - Chromium is closed or crashed
|
|
||||||
*
|
|
||||||
* - The {@link Browser.disconnect | browser.disconnect } method was called.
|
|
||||||
*/
|
|
||||||
Disconnected = 'disconnected',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the url of a target changes. Contains a {@link Target} instance.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* Note that this includes target changes in incognito browser contexts.
|
|
||||||
*/
|
|
||||||
TargetChanged = 'targetchanged',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when a target is created, for example when a new page is opened by
|
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
|
|
||||||
* or by {@link Browser.newPage | browser.newPage}
|
|
||||||
*
|
|
||||||
* Contains a {@link Target} instance.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* Note that this includes target creations in incognito browser contexts.
|
|
||||||
*/
|
|
||||||
TargetCreated = 'targetcreated',
|
|
||||||
/**
|
|
||||||
* Emitted when a target is destroyed, for example when a page is closed.
|
|
||||||
* Contains a {@link Target} instance.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* Note that this includes target destructions in incognito browser contexts.
|
|
||||||
*/
|
|
||||||
TargetDestroyed = 'targetdestroyed',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Browser is created when Puppeteer connects to a Chromium instance, either through
|
|
||||||
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
|
|
||||||
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* An example of using a {@link Browser} to create a {@link Page}:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const puppeteer = require('puppeteer');
|
|
||||||
*
|
|
||||||
* (async () => {
|
|
||||||
* const browser = await puppeteer.launch();
|
|
||||||
* const page = await browser.newPage();
|
|
||||||
* await page.goto('https://example.com');
|
|
||||||
* await browser.close();
|
|
||||||
* })();
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* An example of disconnecting from and reconnecting to a {@link Browser}:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const puppeteer = require('puppeteer');
|
|
||||||
*
|
|
||||||
* (async () => {
|
|
||||||
* const browser = await puppeteer.launch();
|
|
||||||
* // Store the endpoint to be able to reconnect to Chromium
|
|
||||||
* const browserWSEndpoint = browser.wsEndpoint();
|
|
||||||
* // Disconnect puppeteer from Chromium
|
|
||||||
* browser.disconnect();
|
|
||||||
*
|
|
||||||
* // Use the endpoint to reestablish a connection
|
|
||||||
* const browser2 = await puppeteer.connect({browserWSEndpoint});
|
|
||||||
* // Close Chromium
|
|
||||||
* await browser2.close();
|
|
||||||
* })();
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export class Browser extends EventEmitter {
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
@ -230,8 +57,8 @@ export class Browser extends EventEmitter {
|
||||||
closeCallback?: BrowserCloseCallback,
|
closeCallback?: BrowserCloseCallback,
|
||||||
targetFilterCallback?: TargetFilterCallback,
|
targetFilterCallback?: TargetFilterCallback,
|
||||||
isPageTargetCallback?: IsPageTargetCallback
|
isPageTargetCallback?: IsPageTargetCallback
|
||||||
): Promise<Browser> {
|
): Promise<CDPBrowser> {
|
||||||
const browser = new Browser(
|
const browser = new CDPBrowser(
|
||||||
product,
|
product,
|
||||||
connection,
|
connection,
|
||||||
contextIds,
|
contextIds,
|
||||||
|
|
@ -252,15 +79,15 @@ export class Browser extends EventEmitter {
|
||||||
#closeCallback: BrowserCloseCallback;
|
#closeCallback: BrowserCloseCallback;
|
||||||
#targetFilterCallback: TargetFilterCallback;
|
#targetFilterCallback: TargetFilterCallback;
|
||||||
#isPageTargetCallback!: IsPageTargetCallback;
|
#isPageTargetCallback!: IsPageTargetCallback;
|
||||||
#defaultContext: BrowserContext;
|
#defaultContext: CDPBrowserContext;
|
||||||
#contexts: Map<string, BrowserContext>;
|
#contexts: Map<string, CDPBrowserContext>;
|
||||||
#screenshotTaskQueue: TaskQueue;
|
#screenshotTaskQueue: TaskQueue;
|
||||||
#targetManager: TargetManager;
|
#targetManager: TargetManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
get _targets(): Map<string, Target> {
|
override get _targets(): Map<string, Target> {
|
||||||
return this.#targetManager.getAvailableTargets();
|
return this.#targetManager.getAvailableTargets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,12 +132,12 @@ export class Browser extends EventEmitter {
|
||||||
this.#targetFilterCallback
|
this.#targetFilterCallback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.#defaultContext = new BrowserContext(this.#connection, this);
|
this.#defaultContext = new CDPBrowserContext(this.#connection, this);
|
||||||
this.#contexts = new Map();
|
this.#contexts = new Map();
|
||||||
for (const contextId of contextIds) {
|
for (const contextId of contextIds) {
|
||||||
this.#contexts.set(
|
this.#contexts.set(
|
||||||
contextId,
|
contextId,
|
||||||
new BrowserContext(this.#connection, this, contextId)
|
new CDPBrowserContext(this.#connection, this, contextId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +149,7 @@ export class Browser extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async _attach(): Promise<void> {
|
override async _attach(): Promise<void> {
|
||||||
this.#connection.on(
|
this.#connection.on(
|
||||||
ConnectionEmittedEvents.Disconnected,
|
ConnectionEmittedEvents.Disconnected,
|
||||||
this.#emitDisconnected
|
this.#emitDisconnected
|
||||||
|
|
@ -349,7 +176,7 @@ export class Browser extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_detach(): void {
|
override _detach(): void {
|
||||||
this.#connection.off(
|
this.#connection.off(
|
||||||
ConnectionEmittedEvents.Disconnected,
|
ConnectionEmittedEvents.Disconnected,
|
||||||
this.#emitDisconnected
|
this.#emitDisconnected
|
||||||
|
|
@ -376,7 +203,7 @@ export class Browser extends EventEmitter {
|
||||||
* The spawned browser process. Returns `null` if the browser instance was created with
|
* The spawned browser process. Returns `null` if the browser instance was created with
|
||||||
* {@link Puppeteer.connect}.
|
* {@link Puppeteer.connect}.
|
||||||
*/
|
*/
|
||||||
process(): ChildProcess | null {
|
override process(): ChildProcess | null {
|
||||||
return this.#process ?? null;
|
return this.#process ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,7 +229,7 @@ export class Browser extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
override _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
||||||
return this.#isPageTargetCallback;
|
return this.#isPageTargetCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,9 +251,9 @@ export class Browser extends EventEmitter {
|
||||||
* })();
|
* })();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async createIncognitoBrowserContext(
|
override async createIncognitoBrowserContext(
|
||||||
options: BrowserContextOptions = {}
|
options: BrowserContextOptions = {}
|
||||||
): Promise<BrowserContext> {
|
): Promise<CDPBrowserContext> {
|
||||||
const {proxyServer, proxyBypassList} = options;
|
const {proxyServer, proxyBypassList} = options;
|
||||||
|
|
||||||
const {browserContextId} = await this.#connection.send(
|
const {browserContextId} = await this.#connection.send(
|
||||||
|
|
@ -436,7 +263,7 @@ export class Browser extends EventEmitter {
|
||||||
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
|
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const context = new BrowserContext(
|
const context = new CDPBrowserContext(
|
||||||
this.#connection,
|
this.#connection,
|
||||||
this,
|
this,
|
||||||
browserContextId
|
browserContextId
|
||||||
|
|
@ -449,21 +276,21 @@ export class Browser extends EventEmitter {
|
||||||
* Returns an array of all open browser contexts. In a newly created browser, this will
|
* Returns an array of all open browser contexts. In a newly created browser, this will
|
||||||
* return a single instance of {@link BrowserContext}.
|
* return a single instance of {@link BrowserContext}.
|
||||||
*/
|
*/
|
||||||
browserContexts(): BrowserContext[] {
|
override browserContexts(): CDPBrowserContext[] {
|
||||||
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
|
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default browser context. The default browser context cannot be closed.
|
* Returns the default browser context. The default browser context cannot be closed.
|
||||||
*/
|
*/
|
||||||
defaultBrowserContext(): BrowserContext {
|
override defaultBrowserContext(): CDPBrowserContext {
|
||||||
return this.#defaultContext;
|
return this.#defaultContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async _disposeContext(contextId?: string): Promise<void> {
|
override async _disposeContext(contextId?: string): Promise<void> {
|
||||||
if (!contextId) {
|
if (!contextId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -564,7 +391,7 @@ export class Browser extends EventEmitter {
|
||||||
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
|
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
|
||||||
* | browser endpoint}.
|
* | browser endpoint}.
|
||||||
*/
|
*/
|
||||||
wsEndpoint(): string {
|
override wsEndpoint(): string {
|
||||||
return this.#connection.url();
|
return this.#connection.url();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -572,14 +399,14 @@ export class Browser extends EventEmitter {
|
||||||
* Promise which resolves to a new {@link Page} object. The Page is created in
|
* Promise which resolves to a new {@link Page} object. The Page is created in
|
||||||
* a default browser context.
|
* a default browser context.
|
||||||
*/
|
*/
|
||||||
async newPage(): Promise<Page> {
|
override async newPage(): Promise<Page> {
|
||||||
return this.#defaultContext.newPage();
|
return this.#defaultContext.newPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async _createPageInContext(contextId?: string): Promise<Page> {
|
override async _createPageInContext(contextId?: string): Promise<Page> {
|
||||||
const {targetId} = await this.#connection.send('Target.createTarget', {
|
const {targetId} = await this.#connection.send('Target.createTarget', {
|
||||||
url: 'about:blank',
|
url: 'about:blank',
|
||||||
browserContextId: contextId || undefined,
|
browserContextId: contextId || undefined,
|
||||||
|
|
@ -605,7 +432,7 @@ export class Browser extends EventEmitter {
|
||||||
* All active targets inside the Browser. In case of multiple browser contexts, returns
|
* All active targets inside the Browser. In case of multiple browser contexts, returns
|
||||||
* an array with all the targets in all browser contexts.
|
* an array with all the targets in all browser contexts.
|
||||||
*/
|
*/
|
||||||
targets(): Target[] {
|
override targets(): Target[] {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
this.#targetManager.getAvailableTargets().values()
|
this.#targetManager.getAvailableTargets().values()
|
||||||
).filter(target => {
|
).filter(target => {
|
||||||
|
|
@ -616,7 +443,7 @@ export class Browser extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* The target associated with the browser.
|
* The target associated with the browser.
|
||||||
*/
|
*/
|
||||||
target(): Target {
|
override target(): Target {
|
||||||
const browserTarget = this.targets().find(target => {
|
const browserTarget = this.targets().find(target => {
|
||||||
return target.type() === 'browser';
|
return target.type() === 'browser';
|
||||||
});
|
});
|
||||||
|
|
@ -643,7 +470,7 @@ export class Browser extends EventEmitter {
|
||||||
* );
|
* );
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async waitForTarget(
|
override async waitForTarget(
|
||||||
predicate: (x: Target) => boolean | Promise<boolean>,
|
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||||
options: WaitForTargetOptions = {}
|
options: WaitForTargetOptions = {}
|
||||||
): Promise<Target> {
|
): Promise<Target> {
|
||||||
|
|
@ -683,7 +510,7 @@ export class Browser extends EventEmitter {
|
||||||
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
|
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
|
||||||
* here. You can find them using {@link Target.page}.
|
* here. You can find them using {@link Target.page}.
|
||||||
*/
|
*/
|
||||||
async pages(): Promise<Page[]> {
|
override async pages(): Promise<Page[]> {
|
||||||
const contextPages = await Promise.all(
|
const contextPages = await Promise.all(
|
||||||
this.browserContexts().map(context => {
|
this.browserContexts().map(context => {
|
||||||
return context.pages();
|
return context.pages();
|
||||||
|
|
@ -705,7 +532,7 @@ export class Browser extends EventEmitter {
|
||||||
*
|
*
|
||||||
* The format of browser.version() might change with future releases of Chromium.
|
* The format of browser.version() might change with future releases of Chromium.
|
||||||
*/
|
*/
|
||||||
async version(): Promise<string> {
|
override async version(): Promise<string> {
|
||||||
const version = await this.#getVersion();
|
const version = await this.#getVersion();
|
||||||
return version.product;
|
return version.product;
|
||||||
}
|
}
|
||||||
|
|
@ -714,26 +541,27 @@ export class Browser extends EventEmitter {
|
||||||
* The browser's original user agent. Pages can override the browser user agent with
|
* The browser's original user agent. Pages can override the browser user agent with
|
||||||
* {@link Page.setUserAgent}.
|
* {@link Page.setUserAgent}.
|
||||||
*/
|
*/
|
||||||
async userAgent(): Promise<string> {
|
override async userAgent(): Promise<string> {
|
||||||
const version = await this.#getVersion();
|
const version = await this.#getVersion();
|
||||||
return version.userAgent;
|
return version.userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object
|
* Closes Chromium and all of its pages (if any were opened). The
|
||||||
* itself is considered to be disposed and cannot be used anymore.
|
* {@link CDPBrowser} object itself is considered to be disposed and cannot be
|
||||||
|
* used anymore.
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
override async close(): Promise<void> {
|
||||||
await this.#closeCallback.call(null);
|
await this.#closeCallback.call(null);
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnects Puppeteer from the browser, but leaves the Chromium process running.
|
* Disconnects Puppeteer from the browser, but leaves the Chromium process running.
|
||||||
* After calling `disconnect`, the {@link Browser} object is considered disposed and
|
* After calling `disconnect`, the {@link CDPBrowser} object is considered disposed and
|
||||||
* cannot be used anymore.
|
* cannot be used anymore.
|
||||||
*/
|
*/
|
||||||
disconnect(): void {
|
override disconnect(): void {
|
||||||
this.#targetManager.dispose();
|
this.#targetManager.dispose();
|
||||||
this.#connection.dispose();
|
this.#connection.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -741,7 +569,7 @@ export class Browser extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Indicates that the browser is connected.
|
* Indicates that the browser is connected.
|
||||||
*/
|
*/
|
||||||
isConnected(): boolean {
|
override isConnected(): boolean {
|
||||||
return !this.#connection._closed;
|
return !this.#connection._closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -749,75 +577,19 @@ export class Browser extends EventEmitter {
|
||||||
return this.#connection.send('Browser.getVersion');
|
return this.#connection.send('Browser.getVersion');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const enum BrowserContextEmittedEvents {
|
|
||||||
/**
|
|
||||||
* Emitted when the url of a target inside the browser context changes.
|
|
||||||
* Contains a {@link Target} instance.
|
|
||||||
*/
|
|
||||||
TargetChanged = 'targetchanged',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when a target is created within the browser context, for example
|
|
||||||
* when a new page is opened by
|
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
|
|
||||||
* or by {@link BrowserContext.newPage | browserContext.newPage}
|
|
||||||
*
|
|
||||||
* Contains a {@link Target} instance.
|
|
||||||
*/
|
|
||||||
TargetCreated = 'targetcreated',
|
|
||||||
/**
|
|
||||||
* Emitted when a target is destroyed within the browser context, for example
|
|
||||||
* when a page is closed. Contains a {@link Target} instance.
|
|
||||||
*/
|
|
||||||
TargetDestroyed = 'targetdestroyed',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BrowserContexts provide a way to operate multiple independent browser
|
* @internal
|
||||||
* sessions. When a browser is launched, it has a single BrowserContext used by
|
|
||||||
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
|
|
||||||
* in the default browser context.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
|
|
||||||
* will emit various events which are documented in the
|
|
||||||
* {@link BrowserContextEmittedEvents} enum.
|
|
||||||
*
|
|
||||||
* If a page opens another page, e.g. with a `window.open` call, the popup will
|
|
||||||
* belong to the parent page's browser context.
|
|
||||||
*
|
|
||||||
* Puppeteer allows creation of "incognito" browser contexts with
|
|
||||||
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
|
|
||||||
* method. "Incognito" browser contexts don't write any browsing data to disk.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // Create a new incognito browser context
|
|
||||||
* const context = await browser.createIncognitoBrowserContext();
|
|
||||||
* // Create a new page inside context.
|
|
||||||
* const page = await context.newPage();
|
|
||||||
* // ... do stuff with page ...
|
|
||||||
* await page.goto('https://example.com');
|
|
||||||
* // Dispose context once it's no longer needed.
|
|
||||||
* await context.close();
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
export class BrowserContext extends EventEmitter {
|
export class CDPBrowserContext extends BrowserContext {
|
||||||
#connection: Connection;
|
#connection: Connection;
|
||||||
#browser: Browser;
|
#browser: CDPBrowser;
|
||||||
#id?: string;
|
#id?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(connection: Connection, browser: Browser, contextId?: string) {
|
constructor(connection: Connection, browser: CDPBrowser, contextId?: string) {
|
||||||
super();
|
super();
|
||||||
this.#connection = connection;
|
this.#connection = connection;
|
||||||
this.#browser = browser;
|
this.#browser = browser;
|
||||||
|
|
@ -827,7 +599,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* An array of all active targets inside the browser context.
|
* An array of all active targets inside the browser context.
|
||||||
*/
|
*/
|
||||||
targets(): Target[] {
|
override targets(): Target[] {
|
||||||
return this.#browser.targets().filter(target => {
|
return this.#browser.targets().filter(target => {
|
||||||
return target.browserContext() === this;
|
return target.browserContext() === this;
|
||||||
});
|
});
|
||||||
|
|
@ -853,7 +625,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* @returns Promise which resolves to the first target found
|
* @returns Promise which resolves to the first target found
|
||||||
* that matches the `predicate` function.
|
* that matches the `predicate` function.
|
||||||
*/
|
*/
|
||||||
waitForTarget(
|
override waitForTarget(
|
||||||
predicate: (x: Target) => boolean | Promise<boolean>,
|
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||||
options: {timeout?: number} = {}
|
options: {timeout?: number} = {}
|
||||||
): Promise<Target> {
|
): Promise<Target> {
|
||||||
|
|
@ -869,7 +641,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* Non visible pages, such as `"background_page"`, will not be listed here.
|
* Non visible pages, such as `"background_page"`, will not be listed here.
|
||||||
* You can find them using {@link Target.page | the target page}.
|
* You can find them using {@link Target.page | the target page}.
|
||||||
*/
|
*/
|
||||||
async pages(): Promise<Page[]> {
|
override async pages(): Promise<Page[]> {
|
||||||
const pages = await Promise.all(
|
const pages = await Promise.all(
|
||||||
this.targets()
|
this.targets()
|
||||||
.filter(target => {
|
.filter(target => {
|
||||||
|
|
@ -897,7 +669,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* @remarks
|
* @remarks
|
||||||
* The default browser context cannot be closed.
|
* The default browser context cannot be closed.
|
||||||
*/
|
*/
|
||||||
isIncognito(): boolean {
|
override isIncognito(): boolean {
|
||||||
return !!this.#id;
|
return !!this.#id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -915,7 +687,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* @param permissions - An array of permissions to grant.
|
* @param permissions - An array of permissions to grant.
|
||||||
* All permissions that are not listed here will be automatically denied.
|
* All permissions that are not listed here will be automatically denied.
|
||||||
*/
|
*/
|
||||||
async overridePermissions(
|
override async overridePermissions(
|
||||||
origin: string,
|
origin: string,
|
||||||
permissions: Permission[]
|
permissions: Permission[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -946,7 +718,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* context.clearPermissionOverrides();
|
* context.clearPermissionOverrides();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async clearPermissionOverrides(): Promise<void> {
|
override async clearPermissionOverrides(): Promise<void> {
|
||||||
await this.#connection.send('Browser.resetPermissions', {
|
await this.#connection.send('Browser.resetPermissions', {
|
||||||
browserContextId: this.#id || undefined,
|
browserContextId: this.#id || undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -955,14 +727,14 @@ export class BrowserContext extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Creates a new page in the browser context.
|
* Creates a new page in the browser context.
|
||||||
*/
|
*/
|
||||||
newPage(): Promise<Page> {
|
override newPage(): Promise<Page> {
|
||||||
return this.#browser._createPageInContext(this.#id);
|
return this.#browser._createPageInContext(this.#id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The browser this browser context belongs to.
|
* The browser this browser context belongs to.
|
||||||
*/
|
*/
|
||||||
browser(): Browser {
|
override browser(): CDPBrowser {
|
||||||
return this.#browser;
|
return this.#browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -973,7 +745,7 @@ export class BrowserContext extends EventEmitter {
|
||||||
* @remarks
|
* @remarks
|
||||||
* Only incognito browser contexts can be closed.
|
* Only incognito browser contexts can be closed.
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
override async close(): Promise<void> {
|
||||||
assert(this.#id, 'Non-incognito profiles cannot be closed!');
|
assert(this.#id, 'Non-incognito profiles cannot be closed!');
|
||||||
await this.#browser._disposeContext(this.#id);
|
await this.#browser._disposeContext(this.#id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,8 @@ import {debugError} from './util.js';
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
import {isNode} from '../environment.js';
|
import {isNode} from '../environment.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {
|
import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
|
||||||
Browser,
|
import {CDPBrowser} from './Browser.js';
|
||||||
IsPageTargetCallback,
|
|
||||||
TargetFilterCallback,
|
|
||||||
} from './Browser.js';
|
|
||||||
import {Connection} from './Connection.js';
|
import {Connection} from './Connection.js';
|
||||||
import {ConnectionTransport} from './ConnectionTransport.js';
|
import {ConnectionTransport} from './ConnectionTransport.js';
|
||||||
import {getFetch} from './fetch.js';
|
import {getFetch} from './fetch.js';
|
||||||
|
|
@ -55,6 +52,11 @@ export interface BrowserConnectOptions {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_isPageTarget?: IsPageTargetCallback;
|
_isPageTarget?: IsPageTargetCallback;
|
||||||
|
/**
|
||||||
|
* @defaultValue 'cdp'
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protocol?: 'cdp' | 'webDriverBiDi';
|
||||||
}
|
}
|
||||||
|
|
||||||
const getWebSocketTransportClass = async () => {
|
const getWebSocketTransportClass = async () => {
|
||||||
|
|
@ -70,13 +72,13 @@ const getWebSocketTransportClass = async () => {
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export async function _connectToBrowser(
|
export async function _connectToCDPBrowser(
|
||||||
options: BrowserConnectOptions & {
|
options: BrowserConnectOptions & {
|
||||||
browserWSEndpoint?: string;
|
browserWSEndpoint?: string;
|
||||||
browserURL?: string;
|
browserURL?: string;
|
||||||
transport?: ConnectionTransport;
|
transport?: ConnectionTransport;
|
||||||
}
|
}
|
||||||
): Promise<Browser> {
|
): Promise<CDPBrowser> {
|
||||||
const {
|
const {
|
||||||
browserWSEndpoint,
|
browserWSEndpoint,
|
||||||
browserURL,
|
browserURL,
|
||||||
|
|
@ -118,7 +120,7 @@ export async function _connectToBrowser(
|
||||||
const {browserContextIds} = await connection.send(
|
const {browserContextIds} = await connection.send(
|
||||||
'Target.getBrowserContexts'
|
'Target.getBrowserContexts'
|
||||||
);
|
);
|
||||||
const browser = await Browser._create(
|
const browser = await CDPBrowser._create(
|
||||||
product || 'chrome',
|
product || 'chrome',
|
||||||
connection,
|
connection,
|
||||||
browserContextIds,
|
browserContextIds,
|
||||||
|
|
@ -131,7 +133,6 @@ export async function _connectToBrowser(
|
||||||
targetFilter,
|
targetFilter,
|
||||||
isPageTarget
|
isPageTarget
|
||||||
);
|
);
|
||||||
await browser.pages();
|
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {CDPSession, Connection} from './Connection.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter} from './EventEmitter.js';
|
||||||
import {Target} from './Target.js';
|
import {Target} from './Target.js';
|
||||||
import {debugError} from './util.js';
|
import {debugError} from './util.js';
|
||||||
import {TargetFilterCallback} from './Browser.js';
|
import {TargetFilterCallback} from '../api/Browser.js';
|
||||||
import {
|
import {
|
||||||
TargetInterceptor,
|
TargetInterceptor,
|
||||||
TargetFactory,
|
TargetFactory,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export class Connection extends EventEmitter {
|
||||||
#transport: ConnectionTransport;
|
#transport: ConnectionTransport;
|
||||||
#delay: number;
|
#delay: number;
|
||||||
#lastId = 0;
|
#lastId = 0;
|
||||||
#sessions: Map<string, CDPSession> = new Map();
|
#sessions: Map<string, CDPSessionImpl> = new Map();
|
||||||
#closed = false;
|
#closed = false;
|
||||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||||
#manuallyAttached = new Set<string>();
|
#manuallyAttached = new Set<string>();
|
||||||
|
|
@ -147,7 +147,7 @@ export class Connection extends EventEmitter {
|
||||||
const object = JSON.parse(message);
|
const object = JSON.parse(message);
|
||||||
if (object.method === 'Target.attachedToTarget') {
|
if (object.method === 'Target.attachedToTarget') {
|
||||||
const sessionId = object.params.sessionId;
|
const sessionId = object.params.sessionId;
|
||||||
const session = new CDPSession(
|
const session = new CDPSessionImpl(
|
||||||
this,
|
this,
|
||||||
object.params.targetInfo.type,
|
object.params.targetInfo.type,
|
||||||
sessionId
|
sessionId
|
||||||
|
|
@ -310,6 +310,47 @@ export const CDPSessionEmittedEvents = {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class CDPSession extends EventEmitter {
|
export class CDPSession extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connection(): Connection | undefined {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
send<T extends keyof ProtocolMapping.Commands>(
|
||||||
|
method: T,
|
||||||
|
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||||
|
): Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||||
|
send<T extends keyof ProtocolMapping.Commands>(): Promise<
|
||||||
|
ProtocolMapping.Commands[T]['returnType']
|
||||||
|
> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the cdpSession from the target. Once detached, the cdpSession object
|
||||||
|
* won't emit any events and can't be used to send messages.
|
||||||
|
*/
|
||||||
|
async detach(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the session's id.
|
||||||
|
*/
|
||||||
|
id(): string {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class CDPSessionImpl extends CDPSession {
|
||||||
#sessionId: string;
|
#sessionId: string;
|
||||||
#targetType: string;
|
#targetType: string;
|
||||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||||
|
|
@ -325,11 +366,11 @@ export class CDPSession extends EventEmitter {
|
||||||
this.#sessionId = sessionId;
|
this.#sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
connection(): Connection | undefined {
|
override connection(): Connection | undefined {
|
||||||
return this.#connection;
|
return this.#connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
send<T extends keyof ProtocolMapping.Commands>(
|
override send<T extends keyof ProtocolMapping.Commands>(
|
||||||
method: T,
|
method: T,
|
||||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||||
|
|
@ -386,7 +427,7 @@ export class CDPSession extends EventEmitter {
|
||||||
* Detaches the cdpSession from the target. Once detached, the cdpSession object
|
* Detaches the cdpSession from the target. Once detached, the cdpSession object
|
||||||
* won't emit any events and can't be used to send messages.
|
* won't emit any events and can't be used to send messages.
|
||||||
*/
|
*/
|
||||||
async detach(): Promise<void> {
|
override async detach(): Promise<void> {
|
||||||
if (!this.#connection) {
|
if (!this.#connection) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Session already detached. Most likely the ${
|
`Session already detached. Most likely the ${
|
||||||
|
|
@ -419,7 +460,7 @@ export class CDPSession extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Returns the session's id.
|
* Returns the session's id.
|
||||||
*/
|
*/
|
||||||
id(): string {
|
override id(): string {
|
||||||
return this.#sessionId;
|
return this.#sessionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -445,3 +486,13 @@ function rewriteError(
|
||||||
error.originalMessage = originalMessage ?? error.originalMessage;
|
error.originalMessage = originalMessage ?? error.originalMessage;
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function isTargetClosedError(err: Error): boolean {
|
||||||
|
return (
|
||||||
|
err.message.includes('Target closed') ||
|
||||||
|
err.message.includes('Session closed')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,8 @@ export class Coverage {
|
||||||
* Anonymous scripts are ones that don't have an associated url. These are
|
* Anonymous scripts are ones that don't have an associated url. These are
|
||||||
* scripts that are dynamically created on the page using `eval` or
|
* scripts that are dynamically created on the page using `eval` or
|
||||||
* `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
|
* `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
|
||||||
* scripts will have `pptr://__puppeteer_evaluation_script__` as their URL.
|
* scripts URL will start with `debugger://VM` (unless a magic //# sourceURL
|
||||||
|
* comment is present, in which case that will the be URL).
|
||||||
*/
|
*/
|
||||||
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
|
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
|
||||||
return await this.#jsCoverage.start(options);
|
return await this.#jsCoverage.start(options);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* 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 {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {ExecutionContext} from './ExecutionContext.js';
|
import {ExecutionContext} from './ExecutionContext.js';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 mitt, {
|
import mitt, {
|
||||||
Emitter,
|
Emitter,
|
||||||
EventType,
|
EventType,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {IsolatedWorld} from './IsolatedWorld.js';
|
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
import {EvaluateFunc, HandleFor} from './types.js';
|
import {EvaluateFunc, HandleFor} from './types.js';
|
||||||
import {
|
import {
|
||||||
createJSHandle,
|
createJSHandle,
|
||||||
|
|
@ -273,7 +274,7 @@ export class ExecutionContext {
|
||||||
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: functionText + '\n' + suffix + '\n',
|
functionDeclaration: functionText + '\n' + suffix + '\n',
|
||||||
executionContextId: this._contextId,
|
executionContextId: this._contextId,
|
||||||
arguments: args.map(convertArgument.bind(this)),
|
arguments: await Promise.all(args.map(convertArgument.bind(this))),
|
||||||
returnByValue,
|
returnByValue,
|
||||||
awaitPromise: true,
|
awaitPromise: true,
|
||||||
userGesture: true,
|
userGesture: true,
|
||||||
|
|
@ -298,10 +299,13 @@ export class ExecutionContext {
|
||||||
? valueFromRemoteObject(remoteObject)
|
? valueFromRemoteObject(remoteObject)
|
||||||
: createJSHandle(this, remoteObject);
|
: createJSHandle(this, remoteObject);
|
||||||
|
|
||||||
function convertArgument(
|
async function convertArgument(
|
||||||
this: ExecutionContext,
|
this: ExecutionContext,
|
||||||
arg: unknown
|
arg: unknown
|
||||||
): Protocol.Runtime.CallArgument {
|
): Promise<Protocol.Runtime.CallArgument> {
|
||||||
|
if (arg instanceof LazyArg) {
|
||||||
|
arg = await arg.get();
|
||||||
|
}
|
||||||
if (typeof arg === 'bigint') {
|
if (typeof arg === 'bigint') {
|
||||||
// eslint-disable-line valid-typeof
|
// eslint-disable-line valid-typeof
|
||||||
return {unserializableValue: `${arg.toString()}n`};
|
return {unserializableValue: `${arg.toString()}n`};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import Protocol from 'devtools-protocol';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {CDPSession, Connection} from './Connection.js';
|
import {CDPSession, Connection} from './Connection.js';
|
||||||
import {Target} from './Target.js';
|
import {Target} from './Target.js';
|
||||||
import {TargetFilterCallback} from './Browser.js';
|
import {TargetFilterCallback} from '../api/Browser.js';
|
||||||
import {
|
import {
|
||||||
TargetFactory,
|
TargetFactory,
|
||||||
TargetInterceptor,
|
TargetInterceptor,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* 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 {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
|
|
@ -36,7 +52,7 @@ export interface FrameWaitForFunctionOptions {
|
||||||
*
|
*
|
||||||
* - `mutation` - to execute `pageFunction` on every DOM mutation.
|
* - `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||||
*/
|
*/
|
||||||
polling?: string | number;
|
polling?: 'raf' | 'mutation' | number;
|
||||||
/**
|
/**
|
||||||
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
|
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
|
||||||
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
||||||
|
|
@ -150,7 +166,6 @@ export interface FrameAddStyleTagOptions {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class Frame {
|
export class Frame {
|
||||||
#parentFrame: Frame | null;
|
|
||||||
#url = '';
|
#url = '';
|
||||||
#detached = false;
|
#detached = false;
|
||||||
#client!: CDPSession;
|
#client!: CDPSession;
|
||||||
|
|
@ -186,30 +201,25 @@ export class Frame {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_childFrames: Set<Frame>;
|
_parentId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
frameManager: FrameManager,
|
frameManager: FrameManager,
|
||||||
parentFrame: Frame | null,
|
|
||||||
frameId: string,
|
frameId: string,
|
||||||
|
parentFrameId: string | undefined,
|
||||||
client: CDPSession
|
client: CDPSession
|
||||||
) {
|
) {
|
||||||
this._frameManager = frameManager;
|
this._frameManager = frameManager;
|
||||||
this.#parentFrame = parentFrame ?? null;
|
|
||||||
this.#url = '';
|
this.#url = '';
|
||||||
this._id = frameId;
|
this._id = frameId;
|
||||||
|
this._parentId = parentFrameId;
|
||||||
this.#detached = false;
|
this.#detached = false;
|
||||||
|
|
||||||
this._loaderId = '';
|
this._loaderId = '';
|
||||||
|
|
||||||
this._childFrames = new Set();
|
|
||||||
if (this.#parentFrame) {
|
|
||||||
this.#parentFrame._childFrames.add(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateClient(client);
|
this.updateClient(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +230,7 @@ export class Frame {
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
this.worlds = {
|
this.worlds = {
|
||||||
[MAIN_WORLD]: new IsolatedWorld(this),
|
[MAIN_WORLD]: new IsolatedWorld(this),
|
||||||
[PUPPETEER_WORLD]: new IsolatedWorld(this, true),
|
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -664,7 +674,6 @@ export class Frame {
|
||||||
options: FrameWaitForFunctionOptions = {},
|
options: FrameWaitForFunctionOptions = {},
|
||||||
...args: Params
|
...args: Params
|
||||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
// TODO: Fix when NodeHandle has been added.
|
|
||||||
return this.worlds[MAIN_WORLD].waitForFunction(
|
return this.worlds[MAIN_WORLD].waitForFunction(
|
||||||
pageFunction,
|
pageFunction,
|
||||||
options,
|
options,
|
||||||
|
|
@ -721,14 +730,14 @@ export class Frame {
|
||||||
* @returns The parent frame, if any. Detached and main frames return `null`.
|
* @returns The parent frame, if any. Detached and main frames return `null`.
|
||||||
*/
|
*/
|
||||||
parentFrame(): Frame | null {
|
parentFrame(): Frame | null {
|
||||||
return this.#parentFrame;
|
return this._frameManager._frameTree.parentFrame(this._id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns An array of child frames.
|
* @returns An array of child frames.
|
||||||
*/
|
*/
|
||||||
childFrames(): Frame[] {
|
childFrames(): Frame[] {
|
||||||
return Array.from(this._childFrames);
|
return this._frameManager._frameTree.childFrames(this._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -776,8 +785,8 @@ export class Frame {
|
||||||
|
|
||||||
return this.worlds[MAIN_WORLD].transferHandle(
|
return this.worlds[MAIN_WORLD].transferHandle(
|
||||||
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
||||||
async ({url, id, type, content}) => {
|
async ({createDeferredPromise}, {url, id, type, content}) => {
|
||||||
const promise = InjectedUtil.createDeferredPromise<void>();
|
const promise = createDeferredPromise<void>();
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = type;
|
script.type = type;
|
||||||
script.text = content;
|
script.text = content;
|
||||||
|
|
@ -809,6 +818,7 @@ export class Frame {
|
||||||
await promise;
|
await promise;
|
||||||
return script;
|
return script;
|
||||||
},
|
},
|
||||||
|
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
|
||||||
{...options, type, content}
|
{...options, type, content}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -858,8 +868,8 @@ export class Frame {
|
||||||
|
|
||||||
return this.worlds[MAIN_WORLD].transferHandle(
|
return this.worlds[MAIN_WORLD].transferHandle(
|
||||||
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
||||||
async ({url, content}) => {
|
async ({createDeferredPromise}, {url, content}) => {
|
||||||
const promise = InjectedUtil.createDeferredPromise<void>();
|
const promise = createDeferredPromise<void>();
|
||||||
let element: HTMLStyleElement | HTMLLinkElement;
|
let element: HTMLStyleElement | HTMLLinkElement;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
element = document.createElement('style');
|
element = document.createElement('style');
|
||||||
|
|
@ -892,6 +902,7 @@ export class Frame {
|
||||||
await promise;
|
await promise;
|
||||||
return element;
|
return element;
|
||||||
},
|
},
|
||||||
|
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -1089,9 +1100,5 @@ export class Frame {
|
||||||
this.#detached = true;
|
this.#detached = true;
|
||||||
this.worlds[MAIN_WORLD]._detach();
|
this.worlds[MAIN_WORLD]._detach();
|
||||||
this.worlds[PUPPETEER_WORLD]._detach();
|
this.worlds[PUPPETEER_WORLD]._detach();
|
||||||
if (this.#parentFrame) {
|
|
||||||
this.#parentFrame._childFrames.delete(this);
|
|
||||||
}
|
|
||||||
this.#parentFrame = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,12 @@
|
||||||
|
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
|
|
||||||
import {DeferredPromise} from '../util/DeferredPromise.js';
|
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession, isTargetClosedError} from './Connection.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter} from './EventEmitter.js';
|
||||||
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
|
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
|
import {FrameTree} from './FrameTree.js';
|
||||||
import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||||
import {NetworkManager} from './NetworkManager.js';
|
import {NetworkManager} from './NetworkManager.js';
|
||||||
import {Page} from './Page.js';
|
import {Page} from './Page.js';
|
||||||
|
|
@ -60,20 +59,13 @@ export class FrameManager extends EventEmitter {
|
||||||
#page: Page;
|
#page: Page;
|
||||||
#networkManager: NetworkManager;
|
#networkManager: NetworkManager;
|
||||||
#timeoutSettings: TimeoutSettings;
|
#timeoutSettings: TimeoutSettings;
|
||||||
#frames = new Map<string, Frame>();
|
|
||||||
#contextIdToContext = new Map<string, ExecutionContext>();
|
#contextIdToContext = new Map<string, ExecutionContext>();
|
||||||
#isolatedWorlds = new Set<string>();
|
#isolatedWorlds = new Set<string>();
|
||||||
#mainFrame?: Frame;
|
|
||||||
#client: CDPSession;
|
#client: CDPSession;
|
||||||
/**
|
/**
|
||||||
* Keeps track of OOPIF targets/frames (target ID == frame ID for OOPIFs)
|
* @internal
|
||||||
* that are being initialized.
|
|
||||||
*/
|
*/
|
||||||
#framesPendingTargetInit = new Map<string, DeferredPromise<void>>();
|
_frameTree = new FrameTree();
|
||||||
/**
|
|
||||||
* Keeps track of frames that are in the process of being attached in #onFrameAttached.
|
|
||||||
*/
|
|
||||||
#framesPendingAttachment = new Map<string, DeferredPromise<void>>();
|
|
||||||
|
|
||||||
get timeoutSettings(): TimeoutSettings {
|
get timeoutSettings(): TimeoutSettings {
|
||||||
return this.#timeoutSettings;
|
return this.#timeoutSettings;
|
||||||
|
|
@ -140,19 +132,8 @@ export class FrameManager extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(
|
async initialize(client: CDPSession = this.#client): Promise<void> {
|
||||||
targetId: string,
|
|
||||||
client: CDPSession = this.#client
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
if (!this.#framesPendingTargetInit.has(targetId)) {
|
|
||||||
this.#framesPendingTargetInit.set(
|
|
||||||
targetId,
|
|
||||||
createDebuggableDeferredPromise(
|
|
||||||
`Waiting for target frame ${targetId} failed`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await Promise.all([
|
const result = await Promise.all([
|
||||||
client.send('Page.enable'),
|
client.send('Page.enable'),
|
||||||
client.send('Page.getFrameTree'),
|
client.send('Page.getFrameTree'),
|
||||||
|
|
@ -172,18 +153,11 @@ export class FrameManager extends EventEmitter {
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The target might have been closed before the initialization finished.
|
// The target might have been closed before the initialization finished.
|
||||||
if (
|
if (isErrorLike(error) && isTargetClosedError(error)) {
|
||||||
isErrorLike(error) &&
|
|
||||||
(error.message.includes('Target closed') ||
|
|
||||||
error.message.includes('Session closed'))
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
this.#framesPendingTargetInit.get(targetId)?.resolve();
|
|
||||||
this.#framesPendingTargetInit.delete(targetId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,16 +176,17 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
mainFrame(): Frame {
|
mainFrame(): Frame {
|
||||||
assert(this.#mainFrame, 'Requesting main frame too early!');
|
const mainFrame = this._frameTree.getMainFrame();
|
||||||
return this.#mainFrame;
|
assert(mainFrame, 'Requesting main frame too early!');
|
||||||
|
return mainFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
frames(): Frame[] {
|
frames(): Frame[] {
|
||||||
return Array.from(this.#frames.values());
|
return Array.from(this._frameTree.frames());
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(frameId: string): Frame | null {
|
frame(frameId: string): Frame | null {
|
||||||
return this.#frames.get(frameId) || null;
|
return this._frameTree.getById(frameId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAttachedToTarget(target: Target): void {
|
onAttachedToTarget(target: Target): void {
|
||||||
|
|
@ -219,16 +194,16 @@ export class FrameManager extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = this.#frames.get(target._getTargetInfo().targetId);
|
const frame = this.frame(target._getTargetInfo().targetId);
|
||||||
if (frame) {
|
if (frame) {
|
||||||
frame.updateClient(target._session()!);
|
frame.updateClient(target._session()!);
|
||||||
}
|
}
|
||||||
this.setupEventListeners(target._session()!);
|
this.setupEventListeners(target._session()!);
|
||||||
this.initialize(target._getTargetInfo().targetId, target._session());
|
this.initialize(target._session());
|
||||||
}
|
}
|
||||||
|
|
||||||
onDetachedFromTarget(target: Target): void {
|
onDetachedFromTarget(target: Target): void {
|
||||||
const frame = this.#frames.get(target._targetId);
|
const frame = this.frame(target._targetId);
|
||||||
if (frame && frame.isOOPFrame()) {
|
if (frame && frame.isOOPFrame()) {
|
||||||
// When an OOP iframe is removed from the page, it
|
// When an OOP iframe is removed from the page, it
|
||||||
// will only get a Target.detachedFromTarget event.
|
// will only get a Target.detachedFromTarget event.
|
||||||
|
|
@ -237,7 +212,7 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
||||||
const frame = this.#frames.get(event.frameId);
|
const frame = this.frame(event.frameId);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +221,7 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#onFrameStartedLoading(frameId: string): void {
|
#onFrameStartedLoading(frameId: string): void {
|
||||||
const frame = this.#frames.get(frameId);
|
const frame = this.frame(frameId);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +229,7 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#onFrameStoppedLoading(frameId: string): void {
|
#onFrameStoppedLoading(frameId: string): void {
|
||||||
const frame = this.#frames.get(frameId);
|
const frame = this.frame(frameId);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -288,8 +263,8 @@ export class FrameManager extends EventEmitter {
|
||||||
frameId: string,
|
frameId: string,
|
||||||
parentFrameId: string
|
parentFrameId: string
|
||||||
): void {
|
): void {
|
||||||
if (this.#frames.has(frameId)) {
|
let frame = this.frame(frameId);
|
||||||
const frame = this.#frames.get(frameId)!;
|
if (frame) {
|
||||||
if (session && frame.isOOPFrame()) {
|
if (session && frame.isOOPFrame()) {
|
||||||
// If an OOP iframes becomes a normal iframe again
|
// If an OOP iframes becomes a normal iframe again
|
||||||
// it is first attached to the parent page before
|
// it is first attached to the parent page before
|
||||||
|
|
@ -298,86 +273,41 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentFrame = this.#frames.get(parentFrameId);
|
|
||||||
|
|
||||||
const complete = (parentFrame: Frame) => {
|
frame = new Frame(this, frameId, parentFrameId, session);
|
||||||
assert(parentFrame, `Parent frame ${parentFrameId} not found`);
|
this._frameTree.addFrame(frame);
|
||||||
const frame = new Frame(this, parentFrame, frameId, session);
|
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
|
||||||
this.#frames.set(frame._id, frame);
|
|
||||||
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parentFrame) {
|
|
||||||
return complete(parentFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = this.#framesPendingTargetInit.get(parentFrameId);
|
|
||||||
if (frame) {
|
|
||||||
if (!this.#framesPendingAttachment.has(frameId)) {
|
|
||||||
this.#framesPendingAttachment.set(
|
|
||||||
frameId,
|
|
||||||
createDebuggableDeferredPromise(
|
|
||||||
`Waiting for frame ${frameId} to attach failed`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
frame.then(() => {
|
|
||||||
complete(this.#frames.get(parentFrameId)!);
|
|
||||||
this.#framesPendingAttachment.get(frameId)?.resolve();
|
|
||||||
this.#framesPendingAttachment.delete(frameId);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Parent frame ${parentFrameId} not found`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#onFrameNavigated(framePayload: Protocol.Page.Frame): void {
|
async #onFrameNavigated(framePayload: Protocol.Page.Frame): Promise<void> {
|
||||||
const frameId = framePayload.id;
|
const frameId = framePayload.id;
|
||||||
const isMainFrame = !framePayload.parentId;
|
const isMainFrame = !framePayload.parentId;
|
||||||
const frame = isMainFrame ? this.#mainFrame : this.#frames.get(frameId);
|
|
||||||
|
|
||||||
const complete = (frame?: Frame) => {
|
let frame = this._frameTree.getById(frameId);
|
||||||
assert(
|
|
||||||
isMainFrame || frame,
|
|
||||||
`Missing frame isMainFrame=${isMainFrame}, frameId=${frameId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Detach all child frames first.
|
// Detach all child frames first.
|
||||||
if (frame) {
|
if (frame) {
|
||||||
for (const child of frame.childFrames()) {
|
for (const child of frame.childFrames()) {
|
||||||
this.#removeFramesRecursively(child);
|
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 = frameId;
|
|
||||||
} else {
|
|
||||||
// Initial main frame navigation.
|
|
||||||
frame = new Frame(this, null, frameId, this.#client);
|
|
||||||
}
|
|
||||||
this.#frames.set(frameId, frame);
|
|
||||||
this.#mainFrame = frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update frame payload.
|
|
||||||
assert(frame);
|
|
||||||
frame._navigated(framePayload);
|
|
||||||
|
|
||||||
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
|
|
||||||
};
|
|
||||||
const pendingFrame = this.#framesPendingAttachment.get(frameId);
|
|
||||||
if (pendingFrame) {
|
|
||||||
pendingFrame.then(() => {
|
|
||||||
complete(isMainFrame ? this.#mainFrame : this.#frames.get(frameId));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
complete(frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update or create main frame.
|
||||||
|
if (isMainFrame) {
|
||||||
|
if (frame) {
|
||||||
|
// Update frame id to retain frame identity on cross-process navigation.
|
||||||
|
this._frameTree.removeFrame(frame);
|
||||||
|
frame._id = frameId;
|
||||||
|
} else {
|
||||||
|
// Initial main frame navigation.
|
||||||
|
frame = new Frame(this, frameId, undefined, this.#client);
|
||||||
|
}
|
||||||
|
this._frameTree.addFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = await this._frameTree.waitForFrame(frameId);
|
||||||
|
frame._navigated(framePayload);
|
||||||
|
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
|
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
|
||||||
|
|
@ -414,7 +344,7 @@ export class FrameManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
||||||
const frame = this.#frames.get(frameId);
|
const frame = this.frame(frameId);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -427,7 +357,7 @@ export class FrameManager extends EventEmitter {
|
||||||
frameId: string,
|
frameId: string,
|
||||||
reason: Protocol.Page.FrameDetachedEventReason
|
reason: Protocol.Page.FrameDetachedEventReason
|
||||||
): void {
|
): void {
|
||||||
const frame = this.#frames.get(frameId);
|
const frame = this.frame(frameId);
|
||||||
if (reason === 'remove') {
|
if (reason === 'remove') {
|
||||||
// Only remove the frame if the reason for the detached event is
|
// Only remove the frame if the reason for the detached event is
|
||||||
// an actual removement of the frame.
|
// an actual removement of the frame.
|
||||||
|
|
@ -446,8 +376,7 @@ export class FrameManager extends EventEmitter {
|
||||||
): void {
|
): void {
|
||||||
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
|
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
|
||||||
const frameId = auxData && auxData.frameId;
|
const frameId = auxData && auxData.frameId;
|
||||||
const frame =
|
const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
|
||||||
typeof frameId === 'string' ? this.#frames.get(frameId) : undefined;
|
|
||||||
let world: IsolatedWorld | undefined;
|
let world: IsolatedWorld | undefined;
|
||||||
if (frame) {
|
if (frame) {
|
||||||
// Only care about execution contexts created for the current session.
|
// Only care about execution contexts created for the current session.
|
||||||
|
|
@ -513,7 +442,7 @@ export class FrameManager extends EventEmitter {
|
||||||
this.#removeFramesRecursively(child);
|
this.#removeFramesRecursively(child);
|
||||||
}
|
}
|
||||||
frame._detach();
|
frame._detach();
|
||||||
this.#frames.delete(frame._id);
|
this._frameTree.removeFrame(frame);
|
||||||
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
|
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
remote/test/puppeteer/src/common/FrameTree.ts
Normal file
111
remote/test/puppeteer/src/common/FrameTree.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 {
|
||||||
|
createDeferredPromise,
|
||||||
|
DeferredPromise,
|
||||||
|
} from '../util/DeferredPromise.js';
|
||||||
|
import type {Frame} from './Frame.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of the page frame tree and it's is managed by
|
||||||
|
* {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
|
||||||
|
* means that referenced frames might not be in the tree anymore. Thus, the tree
|
||||||
|
* structure is eventually consistent.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class FrameTree {
|
||||||
|
#frames = new Map<string, Frame>();
|
||||||
|
// frameID -> parentFrameID
|
||||||
|
#parentIds = new Map<string, string>();
|
||||||
|
// frameID -> childFrameIDs
|
||||||
|
#childIds = new Map<string, Set<string>>();
|
||||||
|
#mainFrame?: Frame;
|
||||||
|
#waitRequests = new Map<string, Set<DeferredPromise<Frame>>>();
|
||||||
|
|
||||||
|
getMainFrame(): Frame | undefined {
|
||||||
|
return this.#mainFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(frameId: string): Frame | undefined {
|
||||||
|
return this.#frames.get(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise that is resolved once the frame with
|
||||||
|
* the given ID is added to the tree.
|
||||||
|
*/
|
||||||
|
waitForFrame(frameId: string): Promise<Frame> {
|
||||||
|
const frame = this.getById(frameId);
|
||||||
|
if (frame) {
|
||||||
|
return Promise.resolve(frame);
|
||||||
|
}
|
||||||
|
const deferred = createDeferredPromise<Frame>();
|
||||||
|
const callbacks =
|
||||||
|
this.#waitRequests.get(frameId) || new Set<DeferredPromise<Frame>>();
|
||||||
|
callbacks.add(deferred);
|
||||||
|
return deferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
frames(): Frame[] {
|
||||||
|
return Array.from(this.#frames.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
addFrame(frame: Frame): void {
|
||||||
|
this.#frames.set(frame._id, frame);
|
||||||
|
if (frame._parentId) {
|
||||||
|
this.#parentIds.set(frame._id, frame._parentId);
|
||||||
|
if (!this.#childIds.has(frame._parentId)) {
|
||||||
|
this.#childIds.set(frame._parentId, new Set());
|
||||||
|
}
|
||||||
|
this.#childIds.get(frame._parentId)!.add(frame._id);
|
||||||
|
} else {
|
||||||
|
this.#mainFrame = frame;
|
||||||
|
}
|
||||||
|
this.#waitRequests.get(frame._id)?.forEach(request => {
|
||||||
|
return request.resolve(frame);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFrame(frame: Frame): void {
|
||||||
|
this.#frames.delete(frame._id);
|
||||||
|
this.#parentIds.delete(frame._id);
|
||||||
|
if (frame._parentId) {
|
||||||
|
this.#childIds.get(frame._parentId)?.delete(frame._id);
|
||||||
|
} else {
|
||||||
|
this.#mainFrame = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
childFrames(frameId: string): Frame[] {
|
||||||
|
const childIds = this.#childIds.get(frameId);
|
||||||
|
if (!childIds) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(childIds)
|
||||||
|
.map(id => {
|
||||||
|
return this.getById(id);
|
||||||
|
})
|
||||||
|
.filter((frame): frame is Frame => {
|
||||||
|
return frame !== undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parentFrame(frameId: string): Frame | undefined {
|
||||||
|
const parentId = this.#parentIds.get(frameId);
|
||||||
|
return parentId ? this.getById(parentId) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,39 +16,23 @@
|
||||||
|
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {source as injectedSource} from '../generated/injected.js';
|
import {source as injectedSource} from '../generated/injected.js';
|
||||||
|
import type PuppeteerUtil from '../injected/injected.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {TimeoutError} from './Errors.js';
|
|
||||||
import {ExecutionContext} from './ExecutionContext.js';
|
import {ExecutionContext} from './ExecutionContext.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
import {FrameManager} from './FrameManager.js';
|
import {FrameManager} from './FrameManager.js';
|
||||||
import {MouseButton} from './Input.js';
|
import {MouseButton} from './Input.js';
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
||||||
import {TimeoutSettings} from './TimeoutSettings.js';
|
import {TimeoutSettings} from './TimeoutSettings.js';
|
||||||
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
||||||
import {
|
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
|
||||||
createJSHandle,
|
import {TaskManager, WaitTask} from './WaitTask.js';
|
||||||
debugError,
|
|
||||||
isNumber,
|
|
||||||
isString,
|
|
||||||
makePredicateString,
|
|
||||||
pageBindingInitString,
|
|
||||||
} from './util.js';
|
|
||||||
|
|
||||||
// predicateQueryHandler and checkWaitForOptions are declared here so that
|
|
||||||
// TypeScript knows about them when used in the predicate function below.
|
|
||||||
declare const predicateQueryHandler: (
|
|
||||||
element: Element | Document,
|
|
||||||
selector: string
|
|
||||||
) => Promise<Element | Element[] | NodeListOf<Element>>;
|
|
||||||
declare const checkWaitForOptions: (
|
|
||||||
node: Node | null,
|
|
||||||
waitForVisible: boolean,
|
|
||||||
waitForHidden: boolean
|
|
||||||
) => Element | null | boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
|
@ -114,7 +98,6 @@ export interface IsolatedWorldChart {
|
||||||
*/
|
*/
|
||||||
export class IsolatedWorld {
|
export class IsolatedWorld {
|
||||||
#frame: Frame;
|
#frame: Frame;
|
||||||
#injected: boolean;
|
|
||||||
#document?: ElementHandle<Document>;
|
#document?: ElementHandle<Document>;
|
||||||
#context = createDeferredPromise<ExecutionContext>();
|
#context = createDeferredPromise<ExecutionContext>();
|
||||||
#detached = false;
|
#detached = false;
|
||||||
|
|
@ -124,10 +107,15 @@ export class IsolatedWorld {
|
||||||
|
|
||||||
// Contains mapping from functions that should be bound to Puppeteer functions.
|
// Contains mapping from functions that should be bound to Puppeteer functions.
|
||||||
#boundFunctions = new Map<string, Function>();
|
#boundFunctions = new Map<string, Function>();
|
||||||
#waitTasks = new Set<WaitTask>();
|
#taskManager = new TaskManager();
|
||||||
|
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
|
||||||
|
|
||||||
get _waitTasks(): Set<WaitTask> {
|
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
|
||||||
return this.#waitTasks;
|
return this.#puppeteerUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
get taskManager(): TaskManager {
|
||||||
|
return this.#taskManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _boundFunctions(): Map<string, Function> {
|
get _boundFunctions(): Map<string, Function> {
|
||||||
|
|
@ -138,11 +126,10 @@ export class IsolatedWorld {
|
||||||
return `${name}_${contextId}`;
|
return `${name}_${contextId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(frame: Frame, injected = false) {
|
constructor(frame: Frame) {
|
||||||
// Keep own reference to client because it might differ from the FrameManager's
|
// Keep own reference to client because it might differ from the FrameManager's
|
||||||
// client for OOP iframes.
|
// client for OOP iframes.
|
||||||
this.#frame = frame;
|
this.#frame = frame;
|
||||||
this.#injected = injected;
|
|
||||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,17 +151,30 @@ export class IsolatedWorld {
|
||||||
|
|
||||||
clearContext(): void {
|
clearContext(): void {
|
||||||
this.#document = undefined;
|
this.#document = undefined;
|
||||||
|
this.#puppeteerUtil = createDeferredPromise();
|
||||||
this.#context = createDeferredPromise();
|
this.#context = createDeferredPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(context: ExecutionContext): void {
|
setContext(context: ExecutionContext): void {
|
||||||
if (this.#injected) {
|
this.#injectPuppeteerUtil(context);
|
||||||
context.evaluate(injectedSource).catch(debugError);
|
|
||||||
}
|
|
||||||
this.#ctxBindings.clear();
|
this.#ctxBindings.clear();
|
||||||
this.#context.resolve(context);
|
this.#context.resolve(context);
|
||||||
for (const waitTask of this._waitTasks) {
|
}
|
||||||
waitTask.rerun();
|
|
||||||
|
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.#puppeteerUtil.resolve(
|
||||||
|
(await context.evaluateHandle(
|
||||||
|
`(() => {
|
||||||
|
const module = {};
|
||||||
|
${injectedSource}
|
||||||
|
return module.exports.default;
|
||||||
|
})()`
|
||||||
|
)) as JSHandle<PuppeteerUtil>
|
||||||
|
);
|
||||||
|
this.#taskManager.rerunAll();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
debugError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,11 +185,9 @@ export class IsolatedWorld {
|
||||||
_detach(): void {
|
_detach(): void {
|
||||||
this.#detached = true;
|
this.#detached = true;
|
||||||
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
||||||
for (const waitTask of this._waitTasks) {
|
this.#taskManager.terminateAll(
|
||||||
waitTask.terminate(
|
new Error('waitForFunction failed: frame got detached.')
|
||||||
new Error('waitForFunction failed: frame got detached.')
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
executionContext(): Promise<ExecutionContext> {
|
executionContext(): Promise<ExecutionContext> {
|
||||||
|
|
@ -411,8 +409,6 @@ export class IsolatedWorld {
|
||||||
// TODO: In theory, it would be enough to call this just once
|
// TODO: In theory, it would be enough to call this just once
|
||||||
await context._client.send('Runtime.addBinding', {
|
await context._client.send('Runtime.addBinding', {
|
||||||
name,
|
name,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore The protocol definition is not up to date.
|
|
||||||
executionContextName: context._contextName,
|
executionContextName: context._contextName,
|
||||||
});
|
});
|
||||||
await context.evaluate(expression);
|
await context.evaluate(expression);
|
||||||
|
|
@ -420,18 +416,19 @@ export class IsolatedWorld {
|
||||||
// We could have tried to evaluate in a context which was already
|
// We could have tried to evaluate in a context which was already
|
||||||
// destroyed. This happens, for example, if the page is navigated while
|
// destroyed. This happens, for example, if the page is navigated while
|
||||||
// we are trying to add the binding
|
// we are trying to add the binding
|
||||||
const ctxDestroyed = (error as Error).message.includes(
|
if (error instanceof Error) {
|
||||||
'Execution context was destroyed'
|
// Destroyed context.
|
||||||
);
|
if (error.message.includes('Execution context was destroyed')) {
|
||||||
const ctxNotFound = (error as Error).message.includes(
|
return;
|
||||||
'Cannot find context with specified id'
|
}
|
||||||
);
|
// Missing context.
|
||||||
if (ctxDestroyed || ctxNotFound) {
|
if (error.message.includes('Cannot find context with specified id')) {
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
debugError(error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugError(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.#ctxBindings.add(
|
this.#ctxBindings.add(
|
||||||
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
||||||
|
|
@ -476,7 +473,17 @@ export class IsolatedWorld {
|
||||||
throw new Error(`Bound function $name is not found`);
|
throw new Error(`Bound function $name is not found`);
|
||||||
}
|
}
|
||||||
const result = await fn(...args);
|
const result = await fn(...args);
|
||||||
await context.evaluate(deliverResult, name, seq, result);
|
await context.evaluate(
|
||||||
|
(name: string, seq: number, result: unknown) => {
|
||||||
|
// @ts-expect-error Code is evaluated in a different context.
|
||||||
|
const callbacks = self[name].callbacks;
|
||||||
|
callbacks.get(seq).resolve(result);
|
||||||
|
callbacks.delete(seq);
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
seq,
|
||||||
|
result
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The WaitTask may already have been resolved by timing out, or the
|
// The WaitTask may already have been resolved by timing out, or the
|
||||||
// exection context may have been destroyed.
|
// exection context may have been destroyed.
|
||||||
|
|
@ -488,14 +495,6 @@ export class IsolatedWorld {
|
||||||
}
|
}
|
||||||
debugError(error);
|
debugError(error);
|
||||||
}
|
}
|
||||||
function deliverResult(name: string, seq: number, result: unknown): void {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore Code is evaluated in a different context.
|
|
||||||
(globalThis as any)[name].callbacks.get(seq).resolve(result);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore Code is evaluated in a different context.
|
|
||||||
(globalThis as any)[name].callbacks.delete(seq);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async _waitForSelectorInPage(
|
async _waitForSelectorInPage(
|
||||||
|
|
@ -503,59 +502,97 @@ export class IsolatedWorld {
|
||||||
root: ElementHandle<Node> | undefined,
|
root: ElementHandle<Node> | undefined,
|
||||||
selector: string,
|
selector: string,
|
||||||
options: WaitForSelectorOptions,
|
options: WaitForSelectorOptions,
|
||||||
binding?: PageBinding
|
bindings = new Set<(...args: never[]) => unknown>()
|
||||||
): Promise<JSHandle<unknown> | null> {
|
): Promise<JSHandle<unknown> | null> {
|
||||||
const {
|
const {
|
||||||
visible: waitForVisible = false,
|
visible: waitForVisible = false,
|
||||||
hidden: waitForHidden = false,
|
hidden: waitForHidden = false,
|
||||||
timeout = this.#timeoutSettings.timeout(),
|
timeout = this.#timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
|
||||||
const title = `selector \`${selector}\`${
|
try {
|
||||||
waitForHidden ? ' to be hidden' : ''
|
const handle = await this.waitForFunction(
|
||||||
}`;
|
async (PuppeteerUtil, query, selector, root, visible) => {
|
||||||
async function predicate(
|
if (!PuppeteerUtil) {
|
||||||
root: Element | Document,
|
return;
|
||||||
selector: string,
|
}
|
||||||
waitForVisible: boolean,
|
const node = (await PuppeteerUtil.createFunction(query)(
|
||||||
waitForHidden: boolean
|
root || document,
|
||||||
): Promise<Node | null | boolean> {
|
selector,
|
||||||
const node = (await predicateQueryHandler(root, selector)) as Element;
|
PuppeteerUtil
|
||||||
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
)) as Node | null;
|
||||||
|
return PuppeteerUtil.checkVisibility(node, visible);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bindings,
|
||||||
|
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
|
||||||
|
root,
|
||||||
|
timeout,
|
||||||
|
},
|
||||||
|
new LazyArg(async () => {
|
||||||
|
try {
|
||||||
|
// In case CDP fails.
|
||||||
|
return await this.puppeteerUtil;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
queryOne.toString(),
|
||||||
|
selector,
|
||||||
|
root,
|
||||||
|
waitForVisible ? true : waitForHidden ? false : undefined
|
||||||
|
);
|
||||||
|
const elementHandle = handle.asElement();
|
||||||
|
if (!elementHandle) {
|
||||||
|
await handle.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return elementHandle;
|
||||||
|
} catch (error) {
|
||||||
|
if (!isErrorLike(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
const waitTaskOptions: WaitTaskOptions = {
|
|
||||||
isolatedWorld: this,
|
|
||||||
predicateBody: makePredicateString(predicate, queryOne),
|
|
||||||
predicateAcceptsContextElement: true,
|
|
||||||
title,
|
|
||||||
polling,
|
|
||||||
timeout,
|
|
||||||
args: [selector, waitForVisible, waitForHidden],
|
|
||||||
binding,
|
|
||||||
root,
|
|
||||||
};
|
|
||||||
const waitTask = new WaitTask(waitTaskOptions);
|
|
||||||
return waitTask.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForFunction(
|
waitForFunction<
|
||||||
pageFunction: Function | string,
|
Params extends unknown[],
|
||||||
options: {polling?: string | number; timeout?: number} = {},
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||||
...args: unknown[]
|
>(
|
||||||
): Promise<JSHandle> {
|
pageFunction: Func | string,
|
||||||
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
|
options: {
|
||||||
options;
|
polling?: 'raf' | 'mutation' | number;
|
||||||
const waitTaskOptions: WaitTaskOptions = {
|
timeout?: number;
|
||||||
isolatedWorld: this,
|
root?: ElementHandle<Node>;
|
||||||
predicateBody: pageFunction,
|
bindings?: Set<(...args: never[]) => unknown>;
|
||||||
predicateAcceptsContextElement: false,
|
} = {},
|
||||||
title: 'function',
|
...args: Params
|
||||||
polling,
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
timeout,
|
const {
|
||||||
args,
|
polling = 'raf',
|
||||||
};
|
timeout = this.#timeoutSettings.timeout(),
|
||||||
const waitTask = new WaitTask(waitTaskOptions);
|
bindings,
|
||||||
return waitTask.promise;
|
root,
|
||||||
|
} = options;
|
||||||
|
if (typeof polling === 'number' && polling < 0) {
|
||||||
|
throw new Error('Cannot poll with non-positive interval');
|
||||||
|
}
|
||||||
|
const waitTask = new WaitTask(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
bindings,
|
||||||
|
polling,
|
||||||
|
root,
|
||||||
|
timeout,
|
||||||
|
},
|
||||||
|
pageFunction as unknown as
|
||||||
|
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
|
||||||
|
| string,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
return waitTask.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
|
|
@ -593,315 +630,3 @@ export class IsolatedWorld {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export interface WaitTaskOptions {
|
|
||||||
isolatedWorld: IsolatedWorld;
|
|
||||||
predicateBody: Function | string;
|
|
||||||
predicateAcceptsContextElement: boolean;
|
|
||||||
title: string;
|
|
||||||
polling: string | number;
|
|
||||||
timeout: number;
|
|
||||||
binding?: PageBinding;
|
|
||||||
args: unknown[];
|
|
||||||
root?: ElementHandle<Node>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = (): void => {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export class WaitTask {
|
|
||||||
#isolatedWorld: IsolatedWorld;
|
|
||||||
#polling: 'raf' | 'mutation' | number;
|
|
||||||
#timeout: number;
|
|
||||||
#predicateBody: string;
|
|
||||||
#predicateAcceptsContextElement: boolean;
|
|
||||||
#args: unknown[];
|
|
||||||
#binding?: PageBinding;
|
|
||||||
#runCount = 0;
|
|
||||||
#resolve: (x: JSHandle) => void = noop;
|
|
||||||
#reject: (x: Error) => void = noop;
|
|
||||||
#timeoutTimer?: NodeJS.Timeout;
|
|
||||||
#terminated = false;
|
|
||||||
#root: ElementHandle<Node> | null = null;
|
|
||||||
|
|
||||||
promise: Promise<JSHandle>;
|
|
||||||
|
|
||||||
constructor(options: WaitTaskOptions) {
|
|
||||||
if (isString(options.polling)) {
|
|
||||||
assert(
|
|
||||||
options.polling === 'raf' || options.polling === 'mutation',
|
|
||||||
'Unknown polling option: ' + options.polling
|
|
||||||
);
|
|
||||||
} else if (isNumber(options.polling)) {
|
|
||||||
assert(
|
|
||||||
options.polling > 0,
|
|
||||||
'Cannot poll with non-positive interval: ' + options.polling
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error('Unknown polling options: ' + options.polling);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPredicateBody(predicateBody: Function | string) {
|
|
||||||
if (isString(predicateBody)) {
|
|
||||||
return `return (${predicateBody});`;
|
|
||||||
}
|
|
||||||
return `return (${predicateBody})(...args);`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#isolatedWorld = options.isolatedWorld;
|
|
||||||
this.#polling = options.polling;
|
|
||||||
this.#timeout = options.timeout;
|
|
||||||
this.#root = options.root || null;
|
|
||||||
this.#predicateBody = getPredicateBody(options.predicateBody);
|
|
||||||
this.#predicateAcceptsContextElement =
|
|
||||||
options.predicateAcceptsContextElement;
|
|
||||||
this.#args = options.args;
|
|
||||||
this.#binding = options.binding;
|
|
||||||
this.#runCount = 0;
|
|
||||||
this.#isolatedWorld._waitTasks.add(this);
|
|
||||||
if (this.#binding) {
|
|
||||||
this.#isolatedWorld._boundFunctions.set(
|
|
||||||
this.#binding.name,
|
|
||||||
this.#binding.pptrFunction
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 (options.timeout) {
|
|
||||||
const timeoutError = new TimeoutError(
|
|
||||||
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
|
|
||||||
);
|
|
||||||
this.#timeoutTimer = setTimeout(() => {
|
|
||||||
return this.terminate(timeoutError);
|
|
||||||
}, options.timeout);
|
|
||||||
}
|
|
||||||
this.rerun();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminate(error: Error): void {
|
|
||||||
this.#terminated = true;
|
|
||||||
this.#reject(error);
|
|
||||||
this.#cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerun(): Promise<void> {
|
|
||||||
const runCount = ++this.#runCount;
|
|
||||||
let success: JSHandle | null = null;
|
|
||||||
let error: Error | null = null;
|
|
||||||
const context = await this.#isolatedWorld.executionContext();
|
|
||||||
if (this.#terminated || runCount !== this.#runCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.#binding) {
|
|
||||||
await this.#isolatedWorld._addBindingToContext(
|
|
||||||
context,
|
|
||||||
this.#binding.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.#terminated || runCount !== this.#runCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
success = await context.evaluateHandle(
|
|
||||||
waitForPredicatePageFunction,
|
|
||||||
this.#root || null,
|
|
||||||
this.#predicateBody,
|
|
||||||
this.#predicateAcceptsContextElement,
|
|
||||||
this.#polling,
|
|
||||||
this.#timeout,
|
|
||||||
...this.#args
|
|
||||||
);
|
|
||||||
} catch (error_) {
|
|
||||||
error = error_ as 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.#isolatedWorld
|
|
||||||
.evaluate(s => {
|
|
||||||
return !s;
|
|
||||||
}, success)
|
|
||||||
.catch(() => {
|
|
||||||
return true;
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
if (!success) {
|
|
||||||
throw new Error('Assertion: result handle is not available');
|
|
||||||
}
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
if (error.message.includes('TypeError: binding is not a function')) {
|
|
||||||
return this.rerun();
|
|
||||||
}
|
|
||||||
// When frame is detached the task should have been terminated by the IsolatedWorld.
|
|
||||||
// This can fail if we were adding this task while the frame was detached,
|
|
||||||
// so we terminate here instead.
|
|
||||||
if (
|
|
||||||
error.message.includes(
|
|
||||||
'Execution context is not available in detached frame'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.terminate(
|
|
||||||
new Error('waitForFunction failed: frame got detached.')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the page is navigated, the promise is rejected.
|
|
||||||
// We will try again in the new execution context.
|
|
||||||
if (error.message.includes('Execution context was destroyed')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could have tried to evaluate in a context which was already
|
|
||||||
// destroyed.
|
|
||||||
if (error.message.includes('Cannot find context with specified id')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#reject(error);
|
|
||||||
} else {
|
|
||||||
if (!success) {
|
|
||||||
throw new Error('Assertion: result handle is not available');
|
|
||||||
}
|
|
||||||
this.#resolve(success);
|
|
||||||
}
|
|
||||||
this.#cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
#cleanup(): void {
|
|
||||||
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
|
|
||||||
this.#isolatedWorld._waitTasks.delete(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPredicatePageFunction(
|
|
||||||
root: Node | null,
|
|
||||||
predicateBody: string,
|
|
||||||
predicateAcceptsContextElement: boolean,
|
|
||||||
polling: 'raf' | 'mutation' | number,
|
|
||||||
timeout: number,
|
|
||||||
...args: unknown[]
|
|
||||||
): Promise<unknown> {
|
|
||||||
root = root || document;
|
|
||||||
const predicate = new Function('...args', predicateBody);
|
|
||||||
let timedOut = false;
|
|
||||||
if (timeout) {
|
|
||||||
setTimeout(() => {
|
|
||||||
return (timedOut = true);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
switch (polling) {
|
|
||||||
case 'raf':
|
|
||||||
return await pollRaf();
|
|
||||||
case 'mutation':
|
|
||||||
return await pollMutation();
|
|
||||||
default:
|
|
||||||
return await pollInterval(polling);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollMutation(): Promise<unknown> {
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
return Promise.resolve(success);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fulfill = (_?: unknown) => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
const observer = new MutationObserver(async () => {
|
|
||||||
if (timedOut) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill();
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!root) {
|
|
||||||
throw new Error('Root element is not found.');
|
|
||||||
}
|
|
||||||
observer.observe(root, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollRaf(): Promise<unknown> {
|
|
||||||
let fulfill = (_?: unknown): void => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
await onRaf();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
async function onRaf(): Promise<void> {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
fulfill(success);
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(onRaf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollInterval(pollInterval: number): Promise<unknown> {
|
|
||||||
let fulfill = (_?: unknown): void => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
await onTimeout();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
async function onTimeout(): Promise<void> {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
fulfill(success);
|
|
||||||
} else {
|
|
||||||
setTimeout(onTimeout, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
29
remote/test/puppeteer/src/common/LazyArg.ts
Normal file
29
remote/test/puppeteer/src/common/LazyArg.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class LazyArg<T> {
|
||||||
|
#get: () => Promise<T>;
|
||||||
|
constructor(get: () => Promise<T>) {
|
||||||
|
this.#get = get;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): Promise<T> {
|
||||||
|
return this.#get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {HTTPRequest} from './HTTPRequest.js';
|
import {HTTPRequest} from './HTTPRequest.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter} from './EventEmitter.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
|
|
@ -25,6 +24,7 @@ import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
|
||||||
import {debugError, isString} from './util.js';
|
import {debugError, isString} from './util.js';
|
||||||
import {DeferredPromise} from '../util/DeferredPromise.js';
|
import {DeferredPromise} from '../util/DeferredPromise.js';
|
||||||
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
|
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
|
||||||
|
import {CDPSession} from './Connection.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
|
@ -66,13 +66,6 @@ export const NetworkManagerEmittedEvents = {
|
||||||
RequestFinished: Symbol('NetworkManager.RequestFinished'),
|
RequestFinished: Symbol('NetworkManager.RequestFinished'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface CDPSession extends EventEmitter {
|
|
||||||
send<T extends keyof ProtocolMapping.Commands>(
|
|
||||||
method: T,
|
|
||||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
|
||||||
): Promise<ProtocolMapping.Commands[T]['returnType']>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrameManager {
|
interface FrameManager {
|
||||||
frame(frameId: string): Frame | null;
|
frame(frameId: string): Frame | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,12 @@ import {
|
||||||
} from '../util/DeferredPromise.js';
|
} from '../util/DeferredPromise.js';
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
import {Accessibility} from './Accessibility.js';
|
import {Accessibility} from './Accessibility.js';
|
||||||
import {Browser, BrowserContext} from './Browser.js';
|
import type {Browser, BrowserContext} from '../api/Browser.js';
|
||||||
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
|
import {
|
||||||
|
CDPSession,
|
||||||
|
CDPSessionEmittedEvents,
|
||||||
|
isTargetClosedError,
|
||||||
|
} from './Connection.js';
|
||||||
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
||||||
import {Coverage} from './Coverage.js';
|
import {Coverage} from './Coverage.js';
|
||||||
import {Dialog} from './Dialog.js';
|
import {Dialog} from './Dialog.js';
|
||||||
|
|
@ -36,6 +40,7 @@ import {
|
||||||
Frame,
|
Frame,
|
||||||
FrameAddScriptTagOptions,
|
FrameAddScriptTagOptions,
|
||||||
FrameAddStyleTagOptions,
|
FrameAddStyleTagOptions,
|
||||||
|
FrameWaitForFunctionOptions,
|
||||||
} from './Frame.js';
|
} from './Frame.js';
|
||||||
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
|
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
|
||||||
import {HTTPRequest} from './HTTPRequest.js';
|
import {HTTPRequest} from './HTTPRequest.js';
|
||||||
|
|
@ -470,7 +475,15 @@ export class Page extends EventEmitter {
|
||||||
);
|
);
|
||||||
await page.#initialize();
|
await page.#initialize();
|
||||||
if (defaultViewport) {
|
if (defaultViewport) {
|
||||||
await page.setViewport(defaultViewport);
|
try {
|
||||||
|
await page.setViewport(defaultViewport);
|
||||||
|
} catch (err) {
|
||||||
|
if (isErrorLike(err) && isTargetClosedError(err)) {
|
||||||
|
debugError(err);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
@ -645,11 +658,19 @@ export class Page extends EventEmitter {
|
||||||
};
|
};
|
||||||
|
|
||||||
async #initialize(): Promise<void> {
|
async #initialize(): Promise<void> {
|
||||||
await Promise.all([
|
try {
|
||||||
this.#frameManager.initialize(this.#target._targetId),
|
await Promise.all([
|
||||||
this.#client.send('Performance.enable'),
|
this.#frameManager.initialize(),
|
||||||
this.#client.send('Log.enable'),
|
this.#client.send('Performance.enable'),
|
||||||
]);
|
this.#client.send('Log.enable'),
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
if (isErrorLike(err) && isTargetClosedError(err)) {
|
||||||
|
debugError(err);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #onFileChooser(
|
async #onFileChooser(
|
||||||
|
|
@ -3544,32 +3565,14 @@ export class Page extends EventEmitter {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param pageFunction - Function to be evaluated in browser context
|
* @param pageFunction - Function to be evaluated in browser context
|
||||||
* @param options - Optional waiting parameters
|
* @param options - Options for configuring waiting behavior.
|
||||||
*
|
|
||||||
* - `polling` - An interval at which the `pageFunction` is executed, defaults
|
|
||||||
* to `raf`. If `polling` is a number, then it is treated as an interval in
|
|
||||||
* milliseconds at which the function would be executed. If polling is a
|
|
||||||
* string, then it can be one of the following values:
|
|
||||||
* - `raf` - to constantly execute `pageFunction` in
|
|
||||||
* `requestAnimationFrame` callback. This is the tightest polling mode
|
|
||||||
* which is suitable to observe styling changes.
|
|
||||||
* - `mutation`- to execute pageFunction on every DOM mutation.
|
|
||||||
* - `timeout` - maximum time to wait for in milliseconds. Defaults to `30000`
|
|
||||||
* (30 seconds). Pass `0` to disable timeout. The default value can be
|
|
||||||
* changed by using the {@link Page.setDefaultTimeout} method.
|
|
||||||
* @param args - Arguments to pass to `pageFunction`
|
|
||||||
* @returns A `Promise` which resolves to a JSHandle/ElementHandle of the the
|
|
||||||
* `pageFunction`'s return value.
|
|
||||||
*/
|
*/
|
||||||
waitForFunction<
|
waitForFunction<
|
||||||
Params extends unknown[],
|
Params extends unknown[],
|
||||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||||
>(
|
>(
|
||||||
pageFunction: Func | string,
|
pageFunction: Func | string,
|
||||||
options: {
|
options: FrameWaitForFunctionOptions = {},
|
||||||
timeout?: number;
|
|
||||||
polling?: string | number;
|
|
||||||
} = {},
|
|
||||||
...args: Params
|
...args: Params
|
||||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,11 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {Browser} from './Browser.js';
|
import {Browser} from '../api/Browser.js';
|
||||||
import {BrowserConnectOptions, _connectToBrowser} from './BrowserConnector.js';
|
import {
|
||||||
|
BrowserConnectOptions,
|
||||||
|
_connectToCDPBrowser,
|
||||||
|
} from './BrowserConnector.js';
|
||||||
import {ConnectionTransport} from './ConnectionTransport.js';
|
import {ConnectionTransport} from './ConnectionTransport.js';
|
||||||
import {devices} from './DeviceDescriptors.js';
|
import {devices} from './DeviceDescriptors.js';
|
||||||
import {errors} from './Errors.js';
|
import {errors} from './Errors.js';
|
||||||
|
|
@ -54,7 +57,13 @@ export interface ConnectOptions extends BrowserConnectOptions {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class Puppeteer {
|
export class Puppeteer {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
protected _isPuppeteerCore: boolean;
|
protected _isPuppeteerCore: boolean;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
protected _changedProduct = false;
|
protected _changedProduct = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -75,7 +84,7 @@ export class Puppeteer {
|
||||||
* @returns Promise which resolves to browser instance.
|
* @returns Promise which resolves to browser instance.
|
||||||
*/
|
*/
|
||||||
connect(options: ConnectOptions): Promise<Browser> {
|
connect(options: ConnectOptions): Promise<Browser> {
|
||||||
return _connectToBrowser(options);
|
return _connectToCDPBrowser(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import PuppeteerUtil from '../injected/injected.js';
|
||||||
import {ariaHandler} from './AriaQueryHandler.js';
|
import {ariaHandler} from './AriaQueryHandler.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
|
|
@ -41,6 +42,28 @@ export interface CustomQueryHandler {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export interface InternalQueryHandler {
|
export interface InternalQueryHandler {
|
||||||
|
/**
|
||||||
|
* @returns A {@link Node} matching the given `selector` from {@link node}.
|
||||||
|
*/
|
||||||
|
queryOne?: (
|
||||||
|
node: Node,
|
||||||
|
selector: string,
|
||||||
|
PuppeteerUtil: PuppeteerUtil
|
||||||
|
) => Node | null;
|
||||||
|
/**
|
||||||
|
* @returns Some {@link Node}s matching the given `selector` from {@link node}.
|
||||||
|
*/
|
||||||
|
queryAll?: (
|
||||||
|
node: Node,
|
||||||
|
selector: string,
|
||||||
|
PuppeteerUtil: PuppeteerUtil
|
||||||
|
) => Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface PuppeteerQueryHandler {
|
||||||
/**
|
/**
|
||||||
* Queries for a single node given a selector and {@link ElementHandle}.
|
* Queries for a single node given a selector and {@link ElementHandle}.
|
||||||
*
|
*
|
||||||
|
|
@ -71,15 +94,19 @@ export interface InternalQueryHandler {
|
||||||
) => Promise<ElementHandle<Node> | null>;
|
) => Promise<ElementHandle<Node> | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalizeCustomQueryHandler(
|
function createPuppeteerQueryHandler(
|
||||||
handler: CustomQueryHandler
|
handler: InternalQueryHandler
|
||||||
): InternalQueryHandler {
|
): PuppeteerQueryHandler {
|
||||||
const internalHandler: InternalQueryHandler = {};
|
const internalHandler: PuppeteerQueryHandler = {};
|
||||||
|
|
||||||
if (handler.queryOne) {
|
if (handler.queryOne) {
|
||||||
const queryOne = handler.queryOne;
|
const queryOne = handler.queryOne;
|
||||||
internalHandler.queryOne = async (element, selector) => {
|
internalHandler.queryOne = async (element, selector) => {
|
||||||
const jsHandle = await element.evaluateHandle(queryOne, selector);
|
const jsHandle = await element.evaluateHandle(
|
||||||
|
queryOne,
|
||||||
|
selector,
|
||||||
|
await element.executionContext()._world!.puppeteerUtil
|
||||||
|
);
|
||||||
const elementHandle = jsHandle.asElement();
|
const elementHandle = jsHandle.asElement();
|
||||||
if (elementHandle) {
|
if (elementHandle) {
|
||||||
return elementHandle;
|
return elementHandle;
|
||||||
|
|
@ -121,7 +148,11 @@ function internalizeCustomQueryHandler(
|
||||||
if (handler.queryAll) {
|
if (handler.queryAll) {
|
||||||
const queryAll = handler.queryAll;
|
const queryAll = handler.queryAll;
|
||||||
internalHandler.queryAll = async (element, selector) => {
|
internalHandler.queryAll = async (element, selector) => {
|
||||||
const jsHandle = await element.evaluateHandle(queryAll, selector);
|
const jsHandle = await element.evaluateHandle(
|
||||||
|
queryAll,
|
||||||
|
selector,
|
||||||
|
await element.executionContext()._world!.puppeteerUtil
|
||||||
|
);
|
||||||
const properties = await jsHandle.getProperties();
|
const properties = await jsHandle.getProperties();
|
||||||
await jsHandle.dispose();
|
await jsHandle.dispose();
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
@ -138,7 +169,7 @@ function internalizeCustomQueryHandler(
|
||||||
return internalHandler;
|
return internalHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHandler = internalizeCustomQueryHandler({
|
const defaultHandler = createPuppeteerQueryHandler({
|
||||||
queryOne: (element, selector) => {
|
queryOne: (element, selector) => {
|
||||||
if (!('querySelector' in element)) {
|
if (!('querySelector' in element)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -165,87 +196,35 @@ const defaultHandler = internalizeCustomQueryHandler({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pierceHandler = internalizeCustomQueryHandler({
|
const pierceHandler = createPuppeteerQueryHandler({
|
||||||
queryOne: (element, selector) => {
|
queryOne: (element, selector, {pierceQuerySelector}) => {
|
||||||
let found: Node | null = null;
|
return pierceQuerySelector(element, selector);
|
||||||
const search = (root: Node) => {
|
|
||||||
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
||||||
do {
|
|
||||||
const currentNode = iter.currentNode as HTMLElement;
|
|
||||||
if (currentNode.shadowRoot) {
|
|
||||||
search(currentNode.shadowRoot);
|
|
||||||
}
|
|
||||||
if (currentNode instanceof ShadowRoot) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (currentNode !== root && !found && currentNode.matches(selector)) {
|
|
||||||
found = currentNode;
|
|
||||||
}
|
|
||||||
} while (!found && iter.nextNode());
|
|
||||||
};
|
|
||||||
if (element instanceof Document) {
|
|
||||||
element = element.documentElement;
|
|
||||||
}
|
|
||||||
search(element);
|
|
||||||
return found;
|
|
||||||
},
|
},
|
||||||
|
queryAll: (element, selector, {pierceQuerySelectorAll}) => {
|
||||||
queryAll: (element, selector) => {
|
return pierceQuerySelectorAll(element, selector);
|
||||||
const result: Node[] = [];
|
|
||||||
const collect = (root: Node) => {
|
|
||||||
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
||||||
do {
|
|
||||||
const currentNode = iter.currentNode as HTMLElement;
|
|
||||||
if (currentNode.shadowRoot) {
|
|
||||||
collect(currentNode.shadowRoot);
|
|
||||||
}
|
|
||||||
if (currentNode instanceof ShadowRoot) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (currentNode !== root && currentNode.matches(selector)) {
|
|
||||||
result.push(currentNode);
|
|
||||||
}
|
|
||||||
} while (iter.nextNode());
|
|
||||||
};
|
|
||||||
if (element instanceof Document) {
|
|
||||||
element = element.documentElement;
|
|
||||||
}
|
|
||||||
collect(element);
|
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const xpathHandler = internalizeCustomQueryHandler({
|
const xpathHandler = createPuppeteerQueryHandler({
|
||||||
queryOne: (element, selector) => {
|
queryOne: (element, selector, {xpathQuerySelector}) => {
|
||||||
const doc = element.ownerDocument || document;
|
return xpathQuerySelector(element, selector);
|
||||||
const result = doc.evaluate(
|
|
||||||
selector,
|
|
||||||
element,
|
|
||||||
null,
|
|
||||||
XPathResult.FIRST_ORDERED_NODE_TYPE
|
|
||||||
);
|
|
||||||
return result.singleNodeValue;
|
|
||||||
},
|
},
|
||||||
|
queryAll: (element, selector, {xpathQuerySelectorAll}) => {
|
||||||
|
return xpathQuerySelectorAll(element, selector);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
queryAll: (element, selector) => {
|
const textQueryHandler = createPuppeteerQueryHandler({
|
||||||
const doc = element.ownerDocument || document;
|
queryOne: (element, selector, {textQuerySelector}) => {
|
||||||
const iterator = doc.evaluate(
|
return textQuerySelector(element, selector);
|
||||||
selector,
|
},
|
||||||
element,
|
queryAll: (element, selector, {textQuerySelectorAll}) => {
|
||||||
null,
|
return textQuerySelectorAll(element, selector);
|
||||||
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
|
||||||
);
|
|
||||||
const array: Node[] = [];
|
|
||||||
let item;
|
|
||||||
while ((item = iterator.iterateNext())) {
|
|
||||||
array.push(item);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface RegisteredQueryHandler {
|
interface RegisteredQueryHandler {
|
||||||
handler: InternalQueryHandler;
|
handler: PuppeteerQueryHandler;
|
||||||
transformSelector?: (selector: string) => string;
|
transformSelector?: (selector: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,6 +232,7 @@ const INTERNAL_QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>([
|
||||||
['aria', {handler: ariaHandler}],
|
['aria', {handler: ariaHandler}],
|
||||||
['pierce', {handler: pierceHandler}],
|
['pierce', {handler: pierceHandler}],
|
||||||
['xpath', {handler: xpathHandler}],
|
['xpath', {handler: xpathHandler}],
|
||||||
|
['text', {handler: textQueryHandler}],
|
||||||
]);
|
]);
|
||||||
const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>();
|
const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>();
|
||||||
|
|
||||||
|
|
@ -294,7 +274,7 @@ export function registerCustomQueryHandler(
|
||||||
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
|
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
QUERY_HANDLERS.set(name, {handler: internalizeCustomQueryHandler(handler)});
|
QUERY_HANDLERS.set(name, {handler: createPuppeteerQueryHandler(handler)});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -331,7 +311,7 @@ const CUSTOM_QUERY_SEPARATORS = ['=', '/'];
|
||||||
*/
|
*/
|
||||||
export function getQueryHandlerAndSelector(selector: string): {
|
export function getQueryHandlerAndSelector(selector: string): {
|
||||||
updatedSelector: string;
|
updatedSelector: string;
|
||||||
queryHandler: InternalQueryHandler;
|
queryHandler: PuppeteerQueryHandler;
|
||||||
} {
|
} {
|
||||||
for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) {
|
for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) {
|
||||||
for (const [
|
for (const [
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@
|
||||||
import {Page, PageEmittedEvents} from './Page.js';
|
import {Page, PageEmittedEvents} from './Page.js';
|
||||||
import {WebWorker} from './WebWorker.js';
|
import {WebWorker} from './WebWorker.js';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {Browser, BrowserContext, IsPageTargetCallback} from './Browser.js';
|
import type {
|
||||||
|
Browser,
|
||||||
|
BrowserContext,
|
||||||
|
IsPageTargetCallback,
|
||||||
|
} from '../api/Browser.js';
|
||||||
import {Viewport} from './PuppeteerViewport.js';
|
import {Viewport} from './PuppeteerViewport.js';
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
import {TaskQueue} from './TaskQueue.js';
|
import {TaskQueue} from './TaskQueue.js';
|
||||||
|
|
|
||||||
257
remote/test/puppeteer/src/common/WaitTask.ts
Normal file
257
remote/test/puppeteer/src/common/WaitTask.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 type {Poller} from '../injected/Poller.js';
|
||||||
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
|
import {TimeoutError} from './Errors.js';
|
||||||
|
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||||
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {HandleFor} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface WaitTaskOptions {
|
||||||
|
bindings?: Set<(...args: never[]) => unknown>;
|
||||||
|
polling: 'raf' | 'mutation' | number;
|
||||||
|
root?: ElementHandle<Node>;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class WaitTask<T = unknown> {
|
||||||
|
#world: IsolatedWorld;
|
||||||
|
#bindings: Set<(...args: never[]) => unknown>;
|
||||||
|
#polling: 'raf' | 'mutation' | number;
|
||||||
|
#root?: ElementHandle<Node>;
|
||||||
|
|
||||||
|
#fn: string;
|
||||||
|
#args: unknown[];
|
||||||
|
|
||||||
|
#timeout?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
#result = createDeferredPromise<HandleFor<T>>();
|
||||||
|
|
||||||
|
#poller?: JSHandle<Poller<T>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
world: IsolatedWorld,
|
||||||
|
options: WaitTaskOptions,
|
||||||
|
fn: ((...args: unknown[]) => Promise<T>) | string,
|
||||||
|
...args: unknown[]
|
||||||
|
) {
|
||||||
|
this.#world = world;
|
||||||
|
this.#bindings = options.bindings ?? new Set();
|
||||||
|
this.#polling = options.polling;
|
||||||
|
this.#root = options.root;
|
||||||
|
|
||||||
|
switch (typeof fn) {
|
||||||
|
case 'string':
|
||||||
|
this.#fn = `() => {return (${fn});}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.#fn = fn.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.#args = args;
|
||||||
|
|
||||||
|
this.#world.taskManager.add(this);
|
||||||
|
|
||||||
|
if (options.timeout) {
|
||||||
|
this.#timeout = setTimeout(() => {
|
||||||
|
this.terminate(
|
||||||
|
new TimeoutError(`Waiting failed: ${options.timeout}ms exceeded`)
|
||||||
|
);
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#bindings.size !== 0) {
|
||||||
|
for (const fn of this.#bindings) {
|
||||||
|
this.#world._boundFunctions.set(fn.name, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rerun();
|
||||||
|
}
|
||||||
|
|
||||||
|
get result(): Promise<HandleFor<T>> {
|
||||||
|
return this.#result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rerun(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.#bindings.size !== 0) {
|
||||||
|
const context = await this.#world.executionContext();
|
||||||
|
await Promise.all(
|
||||||
|
[...this.#bindings].map(async ({name}) => {
|
||||||
|
return await this.#world._addBindingToContext(context, name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.#polling) {
|
||||||
|
case 'raf':
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({RAFPoller, createFunction}, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new RAFPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'mutation':
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({MutationPoller, createFunction}, root, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new MutationPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
}, root || document);
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#root,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({IntervalPoller, createFunction}, ms, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new IntervalPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
}, ms);
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#polling,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#poller.evaluate(poller => {
|
||||||
|
poller.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.#poller.evaluateHandle(poller => {
|
||||||
|
return poller.result();
|
||||||
|
});
|
||||||
|
this.#result.resolve(result);
|
||||||
|
|
||||||
|
await this.terminate();
|
||||||
|
} catch (error) {
|
||||||
|
const badError = this.getBadError(error);
|
||||||
|
if (badError) {
|
||||||
|
await this.terminate(badError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminate(error?: unknown): Promise<void> {
|
||||||
|
this.#world.taskManager.delete(this);
|
||||||
|
|
||||||
|
if (this.#timeout) {
|
||||||
|
clearTimeout(this.#timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !this.#result.finished()) {
|
||||||
|
this.#result.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#poller) {
|
||||||
|
try {
|
||||||
|
await this.#poller.evaluateHandle(async poller => {
|
||||||
|
await poller.stop();
|
||||||
|
});
|
||||||
|
if (this.#poller) {
|
||||||
|
await this.#poller.dispose();
|
||||||
|
this.#poller = undefined;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors since they most likely come from low-level cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not all errors lead to termination. They usually imply we need to rerun the task.
|
||||||
|
*/
|
||||||
|
getBadError(error: unknown): unknown {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// When frame is detached the task should have been terminated by the IsolatedWorld.
|
||||||
|
// This can fail if we were adding this task while the frame was detached,
|
||||||
|
// so we terminate here instead.
|
||||||
|
if (
|
||||||
|
error.message.includes(
|
||||||
|
'Execution context is not available in detached frame'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return new Error('Waiting failed: Frame detached');
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the page is navigated, the promise is rejected.
|
||||||
|
// We will try again in the new execution context.
|
||||||
|
if (error.message.includes('Execution context was destroyed')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could have tried to evaluate in a context which was already
|
||||||
|
// destroyed.
|
||||||
|
if (error.message.includes('Cannot find context with specified id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class TaskManager {
|
||||||
|
#tasks: Set<WaitTask> = new Set<WaitTask>();
|
||||||
|
|
||||||
|
add(task: WaitTask<any>): void {
|
||||||
|
this.#tasks.add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(task: WaitTask<any>): void {
|
||||||
|
this.#tasks.delete(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminateAll(error?: Error): void {
|
||||||
|
for (const task of this.#tasks) {
|
||||||
|
task.terminate(error);
|
||||||
|
}
|
||||||
|
this.#tasks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async rerunAll(): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
[...this.#tasks].map(task => {
|
||||||
|
return task.rerun();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
remote/test/puppeteer/src/common/bidi/Browser.ts
Normal file
52
remote/test/puppeteer/src/common/bidi/Browser.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {
|
||||||
|
Browser as BrowserBase,
|
||||||
|
BrowserCloseCallback,
|
||||||
|
} from '../../api/Browser.js';
|
||||||
|
import {Connection} from './Connection.js';
|
||||||
|
import {ChildProcess} from 'child_process';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Browser extends BrowserBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
static async create(opts: Options): Promise<Browser> {
|
||||||
|
// TODO: await until the connection is established.
|
||||||
|
return new Browser(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
#process?: ChildProcess;
|
||||||
|
#closeCallback?: BrowserCloseCallback;
|
||||||
|
#connection: Connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
constructor(opts: Options) {
|
||||||
|
super();
|
||||||
|
this.#process = opts.process;
|
||||||
|
this.#closeCallback = opts.closeCallback;
|
||||||
|
this.#connection = opts.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
await this.#closeCallback?.call(null);
|
||||||
|
this.#connection.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
override isConnected(): boolean {
|
||||||
|
return !this.#connection.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
override process(): ChildProcess | null {
|
||||||
|
return this.#process ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
process?: ChildProcess;
|
||||||
|
closeCallback?: BrowserCloseCallback;
|
||||||
|
connection: Connection;
|
||||||
|
}
|
||||||
167
remote/test/puppeteer/src/common/bidi/Connection.ts
Normal file
167
remote/test/puppeteer/src/common/bidi/Connection.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* 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 {debug} from '../Debug.js';
|
||||||
|
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
|
||||||
|
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
|
||||||
|
|
||||||
|
import {ConnectionTransport} from '../ConnectionTransport.js';
|
||||||
|
import {EventEmitter} from '../EventEmitter.js';
|
||||||
|
import {ProtocolError} from '../Errors.js';
|
||||||
|
import {ConnectionCallback} from '../Connection.js';
|
||||||
|
|
||||||
|
interface Command {
|
||||||
|
id: number;
|
||||||
|
method: string;
|
||||||
|
params: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandResponse {
|
||||||
|
id: number;
|
||||||
|
result: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
id: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
stacktrace?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
method: string;
|
||||||
|
params: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Connection extends EventEmitter {
|
||||||
|
#transport: ConnectionTransport;
|
||||||
|
#delay: number;
|
||||||
|
#lastId = 0;
|
||||||
|
#closed = false;
|
||||||
|
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||||
|
|
||||||
|
constructor(transport: ConnectionTransport, delay = 0) {
|
||||||
|
super();
|
||||||
|
this.#delay = delay;
|
||||||
|
|
||||||
|
this.#transport = transport;
|
||||||
|
this.#transport.onmessage = this.onMessage.bind(this);
|
||||||
|
this.#transport.onclose = this.#onClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get closed(): boolean {
|
||||||
|
return this.#closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(method: string, params: object): Promise<any> {
|
||||||
|
const id = ++this.#lastId;
|
||||||
|
const stringifiedMessage = JSON.stringify({
|
||||||
|
id,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
} as Command);
|
||||||
|
debugProtocolSend(stringifiedMessage);
|
||||||
|
this.#transport.send(stringifiedMessage);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.#callbacks.set(id, {
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
error: new ProtocolError(),
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected async onMessage(message: string): Promise<void> {
|
||||||
|
if (this.#delay) {
|
||||||
|
await new Promise(f => {
|
||||||
|
return setTimeout(f, this.#delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
debugProtocolReceive(message);
|
||||||
|
const object = JSON.parse(message) as
|
||||||
|
| Event
|
||||||
|
| ErrorResponse
|
||||||
|
| CommandResponse;
|
||||||
|
if ('id' in object) {
|
||||||
|
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 ('error' in object) {
|
||||||
|
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 = undefined;
|
||||||
|
this.#transport.onclose = undefined;
|
||||||
|
for (const callback of this.#callbacks.values()) {
|
||||||
|
callback.reject(
|
||||||
|
rewriteError(
|
||||||
|
callback.error,
|
||||||
|
`Protocol error (${callback.method}): Connection closed.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.#onClose();
|
||||||
|
this.#transport.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteError(
|
||||||
|
error: ProtocolError,
|
||||||
|
message: string,
|
||||||
|
originalMessage?: string
|
||||||
|
): Error {
|
||||||
|
error.message = message;
|
||||||
|
error.originalMessage = originalMessage ?? error.originalMessage;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProtocolError(
|
||||||
|
error: ProtocolError,
|
||||||
|
method: string,
|
||||||
|
object: ErrorResponse
|
||||||
|
): Error {
|
||||||
|
let message = `Protocol error (${method}): ${object.error} ${object.message}`;
|
||||||
|
if (object.stacktrace) {
|
||||||
|
message += ` ${object.stacktrace}`;
|
||||||
|
}
|
||||||
|
return rewriteError(error, message, object.message);
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
|
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type InnerParams<T extends unknown[]> = {
|
export type InnerParams<T extends unknown[]> = {
|
||||||
[K in keyof T]: FlattenHandle<T[K]>;
|
[K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -249,28 +249,26 @@ export function evaluationString(
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function pageBindingInitString(type: string, name: string): string {
|
export function pageBindingInitString(type: string, name: string): string {
|
||||||
function addPageBinding(type: string, bindingName: string): void {
|
function addPageBinding(type: string, name: string): void {
|
||||||
/* Cast window to any here as we're about to add properties to it
|
// This is the CDP binding.
|
||||||
* via win[bindingName] which TypeScript doesn't like.
|
// @ts-expect-error: In a different context.
|
||||||
*/
|
const callCDP = self[name];
|
||||||
const win = window as any;
|
|
||||||
const binding = win[bindingName];
|
|
||||||
|
|
||||||
win[bindingName] = (...args: unknown[]): Promise<unknown> => {
|
// We replace the CDP binding with a Puppeteer binding.
|
||||||
const me = (window as any)[bindingName];
|
Object.assign(self, {
|
||||||
let callbacks = me.callbacks;
|
[name](...args: unknown[]): Promise<unknown> {
|
||||||
if (!callbacks) {
|
// This is the Puppeteer binding.
|
||||||
callbacks = new Map();
|
// @ts-expect-error: In a different context.
|
||||||
me.callbacks = callbacks;
|
const callPuppeteer = self[name];
|
||||||
}
|
callPuppeteer.callbacks ??= new Map();
|
||||||
const seq = (me.lastSeq || 0) + 1;
|
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
|
||||||
me.lastSeq = seq;
|
callPuppeteer.lastSeq = seq;
|
||||||
const promise = new Promise((resolve, reject) => {
|
callCDP(JSON.stringify({type, name, seq, args}));
|
||||||
return callbacks.set(seq, {resolve, reject});
|
return new Promise((resolve, reject) => {
|
||||||
});
|
callPuppeteer.callbacks.set(seq, {resolve, reject});
|
||||||
binding(JSON.stringify({type, name: bindingName, seq, args}));
|
});
|
||||||
return promise;
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
return evaluationString(addPageBinding, type, name);
|
return evaluationString(addPageBinding, type, name);
|
||||||
}
|
}
|
||||||
|
|
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
|
||||||
return evaluationString(deliverErrorValue, name, seq, value);
|
return evaluationString(deliverErrorValue, name, seq, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export function makePredicateString(
|
|
||||||
predicate: Function,
|
|
||||||
predicateQueryHandler: Function
|
|
||||||
): string {
|
|
||||||
function checkWaitForOptions(
|
|
||||||
node: Node | null,
|
|
||||||
waitForVisible: boolean,
|
|
||||||
waitForHidden: boolean
|
|
||||||
): Node | null | boolean {
|
|
||||||
if (!node) {
|
|
||||||
return waitForHidden;
|
|
||||||
}
|
|
||||||
if (!waitForVisible && !waitForHidden) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
const element =
|
|
||||||
node.nodeType === Node.TEXT_NODE
|
|
||||||
? (node.parentElement as Element)
|
|
||||||
: (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
(() => {
|
|
||||||
const predicateQueryHandler = ${predicateQueryHandler};
|
|
||||||
const checkWaitForOptions = ${checkWaitForOptions};
|
|
||||||
return (${predicate})(...args)
|
|
||||||
})() `;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
16
remote/test/puppeteer/src/compat.d.ts
vendored
16
remote/test/puppeteer/src/compat.d.ts
vendored
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
declare const puppeteerDirname: string;
|
declare const puppeteerDirname: string;
|
||||||
|
|
||||||
export {puppeteerDirname};
|
export {puppeteerDirname};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 {dirname} from 'path';
|
import {dirname} from 'path';
|
||||||
import {puppeteerDirname} from './compat.js';
|
import {puppeteerDirname} from './compat.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const packageVersion = '17.1.2';
|
export const packageVersion = '18.0.0';
|
||||||
|
|
|
||||||
67
remote/test/puppeteer/src/injected/PierceQuerySelector.ts
Normal file
67
remote/test/puppeteer/src/injected/PierceQuerySelector.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2022 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 const pierceQuerySelector = (
|
||||||
|
root: Node,
|
||||||
|
selector: string
|
||||||
|
): Element | null => {
|
||||||
|
let found: Node | null = null;
|
||||||
|
const search = (root: Node) => {
|
||||||
|
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
|
do {
|
||||||
|
const currentNode = iter.currentNode as Element;
|
||||||
|
if (currentNode.shadowRoot) {
|
||||||
|
search(currentNode.shadowRoot);
|
||||||
|
}
|
||||||
|
if (currentNode instanceof ShadowRoot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentNode !== root && !found && currentNode.matches(selector)) {
|
||||||
|
found = currentNode;
|
||||||
|
}
|
||||||
|
} while (!found && iter.nextNode());
|
||||||
|
};
|
||||||
|
if (root instanceof Document) {
|
||||||
|
root = root.documentElement;
|
||||||
|
}
|
||||||
|
search(root);
|
||||||
|
return found;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pierceQuerySelectorAll = (
|
||||||
|
element: Node,
|
||||||
|
selector: string
|
||||||
|
): Element[] => {
|
||||||
|
const result: Element[] = [];
|
||||||
|
const collect = (root: Node) => {
|
||||||
|
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
|
do {
|
||||||
|
const currentNode = iter.currentNode as Element;
|
||||||
|
if (currentNode.shadowRoot) {
|
||||||
|
collect(currentNode.shadowRoot);
|
||||||
|
}
|
||||||
|
if (currentNode instanceof ShadowRoot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentNode !== root && currentNode.matches(selector)) {
|
||||||
|
result.push(currentNode);
|
||||||
|
}
|
||||||
|
} while (iter.nextNode());
|
||||||
|
};
|
||||||
|
if (element instanceof Document) {
|
||||||
|
element = element.documentElement;
|
||||||
|
}
|
||||||
|
collect(element);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
@ -1,15 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 '../util/assert.js';
|
||||||
import {
|
import {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
DeferredPromise,
|
DeferredPromise,
|
||||||
} from '../util/DeferredPromise.js';
|
} from '../util/DeferredPromise.js';
|
||||||
import {assert} from '../util/assert.js';
|
|
||||||
|
|
||||||
interface Poller<T> {
|
/**
|
||||||
start(): Promise<T>;
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface Poller<T> {
|
||||||
|
start(): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
result(): Promise<T>;
|
result(): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export class MutationPoller<T> implements Poller<T> {
|
export class MutationPoller<T> implements Poller<T> {
|
||||||
#fn: () => Promise<T>;
|
#fn: () => Promise<T>;
|
||||||
|
|
||||||
|
|
@ -22,12 +44,12 @@ export class MutationPoller<T> implements Poller<T> {
|
||||||
this.#root = root;
|
this.#root = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#observer = new MutationObserver(async () => {
|
this.#observer = new MutationObserver(async () => {
|
||||||
|
|
@ -43,8 +65,6 @@ export class MutationPoller<T> implements Poller<T> {
|
||||||
subtree: true,
|
subtree: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
|
|
@ -54,6 +74,7 @@ export class MutationPoller<T> implements Poller<T> {
|
||||||
}
|
}
|
||||||
if (this.#observer) {
|
if (this.#observer) {
|
||||||
this.#observer.disconnect();
|
this.#observer.disconnect();
|
||||||
|
this.#observer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,12 +91,12 @@ export class RAFPoller<T> implements Poller<T> {
|
||||||
this.#fn = fn;
|
this.#fn = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
|
|
@ -91,8 +112,6 @@ export class RAFPoller<T> implements Poller<T> {
|
||||||
await this.stop();
|
await this.stop();
|
||||||
};
|
};
|
||||||
window.requestAnimationFrame(poll);
|
window.requestAnimationFrame(poll);
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
|
|
@ -119,12 +138,12 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||||
this.#ms = ms;
|
this.#ms = ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#interval = setInterval(async () => {
|
this.#interval = setInterval(async () => {
|
||||||
|
|
@ -135,8 +154,6 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
await this.stop();
|
await this.stop();
|
||||||
}, this.#ms);
|
}, this.#ms);
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
|
|
@ -146,6 +163,7 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||||
}
|
}
|
||||||
if (this.#interval) {
|
if (this.#interval) {
|
||||||
clearInterval(this.#interval);
|
clearInterval(this.#interval);
|
||||||
|
this.#interval = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
153
remote/test/puppeteer/src/injected/TextContent.ts
Normal file
153
remote/test/puppeteer/src/injected/TextContent.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 NonTrivialValueNode extends Node {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the node has a non-trivial value property.
|
||||||
|
*/
|
||||||
|
const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => {
|
||||||
|
if (node instanceof HTMLSelectElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node instanceof HTMLTextAreaElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node instanceof HTMLInputElement &&
|
||||||
|
!TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a given node is suitable for text matching.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isSuitableNodeForTextMatching = (node: Node): boolean => {
|
||||||
|
return (
|
||||||
|
!UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type TextContent = {
|
||||||
|
// Contains the full text of the node.
|
||||||
|
full: string;
|
||||||
|
// Contains the text immediately beneath the node.
|
||||||
|
immediate: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps {@link Node}s to their computed {@link TextContent}.
|
||||||
|
*/
|
||||||
|
const textContentCache = new WeakMap<Node, TextContent>();
|
||||||
|
const eraseFromCache = (node: Node | null) => {
|
||||||
|
while (node) {
|
||||||
|
textContentCache.delete(node);
|
||||||
|
if (node instanceof ShadowRoot) {
|
||||||
|
node = node.host;
|
||||||
|
} else {
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erases the cache when the tree has mutated text.
|
||||||
|
*/
|
||||||
|
const observedNodes = new WeakSet<Node>();
|
||||||
|
const textChangeObserver = new MutationObserver(mutations => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
eraseFromCache(mutation.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the text content of a node using some custom logic.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The primary reason this function exists is due to {@link ShadowRoot}s not having
|
||||||
|
* text content.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const createTextContent = (root: Node): TextContent => {
|
||||||
|
let value = textContentCache.get(root);
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
value = {full: '', immediate: []};
|
||||||
|
if (!isSuitableNodeForTextMatching(root)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentImmediate = '';
|
||||||
|
if (isNonTrivialValueNode(root)) {
|
||||||
|
value.full = root.value;
|
||||||
|
value.immediate.push(root.value);
|
||||||
|
|
||||||
|
root.addEventListener(
|
||||||
|
'input',
|
||||||
|
event => {
|
||||||
|
eraseFromCache(event.target as HTMLInputElement);
|
||||||
|
},
|
||||||
|
{once: true, capture: true}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (let child = root.firstChild; child; child = child.nextSibling) {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
value.full += child.nodeValue ?? '';
|
||||||
|
currentImmediate += child.nodeValue ?? '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentImmediate) {
|
||||||
|
value.immediate.push(currentImmediate);
|
||||||
|
}
|
||||||
|
currentImmediate = '';
|
||||||
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
value.full += createTextContent(child).full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentImmediate) {
|
||||||
|
value.immediate.push(currentImmediate);
|
||||||
|
}
|
||||||
|
if (root instanceof Element && root.shadowRoot) {
|
||||||
|
value.full += createTextContent(root.shadowRoot).full;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!observedNodes.has(root)) {
|
||||||
|
textChangeObserver.observe(root, {
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
observedNodes.add(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textContentCache.set(root, value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
86
remote/test/puppeteer/src/injected/TextQuerySelector.ts
Normal file
86
remote/test/puppeteer/src/injected/TextQuerySelector.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 {
|
||||||
|
createTextContent,
|
||||||
|
isSuitableNodeForTextMatching,
|
||||||
|
} from './TextContent.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the given node for a node matching the given text selector.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const textQuerySelector = (
|
||||||
|
root: Node,
|
||||||
|
selector: string
|
||||||
|
): Element | null => {
|
||||||
|
for (const node of root.childNodes) {
|
||||||
|
if (node instanceof Element && isSuitableNodeForTextMatching(node)) {
|
||||||
|
let matchedNode: Element | null;
|
||||||
|
if (node.shadowRoot) {
|
||||||
|
matchedNode = textQuerySelector(node.shadowRoot, selector);
|
||||||
|
} else {
|
||||||
|
matchedNode = textQuerySelector(node, selector);
|
||||||
|
}
|
||||||
|
if (matchedNode) {
|
||||||
|
return matchedNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root instanceof Element) {
|
||||||
|
const textContent = createTextContent(root);
|
||||||
|
if (textContent.full.includes(selector)) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the given node for all nodes matching the given text selector.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const textQuerySelectorAll = (
|
||||||
|
root: Node,
|
||||||
|
selector: string
|
||||||
|
): Element[] => {
|
||||||
|
let results: Element[] = [];
|
||||||
|
for (const node of root.childNodes) {
|
||||||
|
if (node instanceof Element) {
|
||||||
|
let matchedNodes: Element[];
|
||||||
|
if (node.shadowRoot) {
|
||||||
|
matchedNodes = textQuerySelectorAll(node.shadowRoot, selector);
|
||||||
|
} else {
|
||||||
|
matchedNodes = textQuerySelectorAll(node, selector);
|
||||||
|
}
|
||||||
|
results = results.concat(matchedNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (results.length > 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root instanceof Element) {
|
||||||
|
const textContent = createTextContent(root);
|
||||||
|
if (textContent.full.includes(selector)) {
|
||||||
|
return [root];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
45
remote/test/puppeteer/src/injected/XPathQuerySelector.ts
Normal file
45
remote/test/puppeteer/src/injected/XPathQuerySelector.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 const xpathQuerySelector = (
|
||||||
|
root: Node,
|
||||||
|
selector: string
|
||||||
|
): Node | null => {
|
||||||
|
const doc = root.ownerDocument || document;
|
||||||
|
const result = doc.evaluate(
|
||||||
|
selector,
|
||||||
|
root,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE
|
||||||
|
);
|
||||||
|
return result.singleNodeValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const xpathQuerySelectorAll = (root: Node, selector: string): Node[] => {
|
||||||
|
const doc = root.ownerDocument || document;
|
||||||
|
const iterator = doc.evaluate(
|
||||||
|
selector,
|
||||||
|
root,
|
||||||
|
null,
|
||||||
|
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||||
|
);
|
||||||
|
const array: Node[] = [];
|
||||||
|
let item;
|
||||||
|
while ((item = iterator.iterateNext())) {
|
||||||
|
array.push(item);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
@ -1,14 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 {createDeferredPromise} from '../util/DeferredPromise.js';
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
import * as Poller from './Poller.js';
|
import * as Poller from './Poller.js';
|
||||||
|
import * as TextContent from './TextContent.js';
|
||||||
|
import * as TextQuerySelector from './TextQuerySelector.js';
|
||||||
|
import * as XPathQuerySelector from './XPathQuerySelector.js';
|
||||||
|
import * as PierceQuerySelector from './PierceQuerySelector.js';
|
||||||
import * as util from './util.js';
|
import * as util from './util.js';
|
||||||
|
|
||||||
Object.assign(
|
const PuppeteerUtil = Object.freeze({
|
||||||
self,
|
...util,
|
||||||
Object.freeze({
|
...Poller,
|
||||||
InjectedUtil: {
|
...TextContent,
|
||||||
...Poller,
|
...TextQuerySelector,
|
||||||
...util,
|
...XPathQuerySelector,
|
||||||
createDeferredPromise,
|
...PierceQuerySelector,
|
||||||
},
|
createDeferredPromise,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
type PuppeteerUtil = typeof PuppeteerUtil;
|
||||||
|
|
||||||
|
export default PuppeteerUtil;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
|
const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a function from a string.
|
* Creates a function from a string.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const createFunction = (
|
export const createFunction = (
|
||||||
functionValue: string
|
functionValue: string
|
||||||
|
|
@ -16,3 +34,42 @@ export const createFunction = (
|
||||||
createdFunctions.set(functionValue, fn);
|
createdFunctions.set(functionValue, fn);
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const checkVisibility = (
|
||||||
|
node: Node | null,
|
||||||
|
visible?: boolean
|
||||||
|
): Node | boolean => {
|
||||||
|
if (!node) {
|
||||||
|
return visible === false;
|
||||||
|
}
|
||||||
|
if (visible === undefined) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const element = (
|
||||||
|
node.nodeType === Node.TEXT_NODE ? node.parentElement : node
|
||||||
|
) as Element;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const isVisible =
|
||||||
|
style &&
|
||||||
|
!HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
|
||||||
|
isBoundingBoxVisible(element);
|
||||||
|
return visible === isVisible ? node : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isBoundingBoxVisible(element: Element): boolean {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
rect.right > 0 &&
|
||||||
|
rect.bottom > 0 &&
|
||||||
|
rect.left < self.innerWidth &&
|
||||||
|
rect.top < self.innerHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,12 @@ export class BrowserFetcher {
|
||||||
this.#platform = 'linux';
|
this.#platform = 'linux';
|
||||||
break;
|
break;
|
||||||
case 'win32':
|
case 'win32':
|
||||||
this.#platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
this.#platform =
|
||||||
|
os.arch() === 'x64' ||
|
||||||
|
// Windows 11 for ARM supports x64 emulation
|
||||||
|
(os.arch() === 'arm64' && _isWindows11(os.release()))
|
||||||
|
? 'win64'
|
||||||
|
: 'win32';
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
assert(false, 'Unsupported platform: ' + platform);
|
assert(false, 'Unsupported platform: ' + platform);
|
||||||
|
|
@ -336,7 +341,7 @@ export class BrowserFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use system Chromium builds on Linux ARM devices
|
// Use system Chromium builds on Linux ARM devices
|
||||||
if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
|
if (os.platform() === 'linux' && os.arch() === 'arm64') {
|
||||||
handleArm64();
|
handleArm64();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -497,6 +502,25 @@ function parseFolderPath(
|
||||||
return {product, platform, revision};
|
return {product, platform, revision};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 11 is identified by 10.0.22000 or greater
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
function _isWindows11(version: string): boolean {
|
||||||
|
const parts = version.split('.');
|
||||||
|
if (parts.length > 2) {
|
||||||
|
const major = parseInt(parts[0] as string, 10);
|
||||||
|
const minor = parseInt(parts[1] as string, 10);
|
||||||
|
const patch = parseInt(parts[2] as string, 10);
|
||||||
|
return (
|
||||||
|
major > 10 ||
|
||||||
|
(major === 10 && minor > 0) ||
|
||||||
|
(major === 10 && minor === 0 && patch >= 22000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import removeFolder from 'rimraf';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {Connection} from '../common/Connection.js';
|
import {Connection} from '../common/Connection.js';
|
||||||
|
import {Connection as BiDiConnection} from '../common/bidi/Connection.js';
|
||||||
import {debug} from '../common/Debug.js';
|
import {debug} from '../common/Debug.js';
|
||||||
import {TimeoutError} from '../common/Errors.js';
|
import {TimeoutError} from '../common/Errors.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -245,6 +246,25 @@ export class BrowserRunner {
|
||||||
removeEventListeners(this.#listeners);
|
removeEventListeners(this.#listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupWebDriverBiDiConnection(options: {
|
||||||
|
timeout: number;
|
||||||
|
slowMo: number;
|
||||||
|
preferredRevision: string;
|
||||||
|
}): Promise<BiDiConnection> {
|
||||||
|
assert(this.proc, 'BrowserRunner not started.');
|
||||||
|
|
||||||
|
const {timeout, slowMo, preferredRevision} = options;
|
||||||
|
let browserWSEndpoint = await waitForWSEndpoint(
|
||||||
|
this.proc,
|
||||||
|
timeout,
|
||||||
|
preferredRevision,
|
||||||
|
/^WebDriver BiDi listening on (ws:\/\/.*)$/
|
||||||
|
);
|
||||||
|
browserWSEndpoint += '/session';
|
||||||
|
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||||
|
return new BiDiConnection(transport, slowMo);
|
||||||
|
}
|
||||||
|
|
||||||
async setupConnection(options: {
|
async setupConnection(options: {
|
||||||
usePipe?: boolean;
|
usePipe?: boolean;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
|
@ -279,7 +299,8 @@ export class BrowserRunner {
|
||||||
function waitForWSEndpoint(
|
function waitForWSEndpoint(
|
||||||
browserProcess: childProcess.ChildProcess,
|
browserProcess: childProcess.ChildProcess,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
preferredRevision: string
|
preferredRevision: string,
|
||||||
|
regex = /^DevTools listening on (ws:\/\/.*)$/
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
|
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
|
||||||
const rl = readline.createInterface(browserProcess.stderr);
|
const rl = readline.createInterface(browserProcess.stderr);
|
||||||
|
|
@ -327,7 +348,7 @@ function waitForWSEndpoint(
|
||||||
|
|
||||||
function onLine(line: string): void {
|
function onLine(line: string): void {
|
||||||
stderr += line + '\n';
|
stderr += line + '\n';
|
||||||
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
|
const match = line.match(regex);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {Browser} from '../common/Browser.js';
|
import {CDPBrowser} from '../common/Browser.js';
|
||||||
import {Product} from '../common/Product.js';
|
import {Product} from '../common/Product.js';
|
||||||
import {BrowserRunner} from './BrowserRunner.js';
|
import {BrowserRunner} from './BrowserRunner.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -43,7 +43,7 @@ export class ChromeLauncher implements ProductLauncher {
|
||||||
this._isPuppeteerCore = isPuppeteerCore;
|
this._isPuppeteerCore = isPuppeteerCore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
|
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<CDPBrowser> {
|
||||||
const {
|
const {
|
||||||
ignoreDefaultArgs = false,
|
ignoreDefaultArgs = false,
|
||||||
args = [],
|
args = [],
|
||||||
|
|
@ -154,7 +154,7 @@ export class ChromeLauncher implements ProductLauncher {
|
||||||
slowMo,
|
slowMo,
|
||||||
preferredRevision: this._preferredRevision,
|
preferredRevision: this._preferredRevision,
|
||||||
});
|
});
|
||||||
browser = await Browser._create(
|
browser = await CDPBrowser._create(
|
||||||
this.product,
|
this.product,
|
||||||
connection,
|
connection,
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {Browser} from '../common/Browser.js';
|
import {Browser} from '../api/Browser.js';
|
||||||
|
import {CDPBrowser as CDPBrowser} from '../common/Browser.js';
|
||||||
|
import {Browser as BiDiBrowser} from '../common/bidi/Browser.js';
|
||||||
import {Product} from '../common/Product.js';
|
import {Product} from '../common/Product.js';
|
||||||
import {BrowserFetcher} from './BrowserFetcher.js';
|
import {BrowserFetcher} from './BrowserFetcher.js';
|
||||||
import {BrowserRunner} from './BrowserRunner.js';
|
import {BrowserRunner} from './BrowserRunner.js';
|
||||||
|
|
@ -58,6 +60,7 @@ export class FirefoxLauncher implements ProductLauncher {
|
||||||
extraPrefsFirefox = {},
|
extraPrefsFirefox = {},
|
||||||
waitForInitialPage = true,
|
waitForInitialPage = true,
|
||||||
debuggingPort = null,
|
debuggingPort = null,
|
||||||
|
protocol = 'cdp',
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const firefoxArguments = [];
|
const firefoxArguments = [];
|
||||||
|
|
@ -113,7 +116,9 @@ export class FirefoxLauncher implements ProductLauncher {
|
||||||
firefoxArguments.push(userDataDir);
|
firefoxArguments.push(userDataDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._updateRevision();
|
if (!this._isPuppeteerCore) {
|
||||||
|
await this._updateRevision();
|
||||||
|
}
|
||||||
let firefoxExecutable = executablePath;
|
let firefoxExecutable = executablePath;
|
||||||
if (!executablePath) {
|
if (!executablePath) {
|
||||||
const {missingText, executablePath} = resolveExecutablePath(this);
|
const {missingText, executablePath} = resolveExecutablePath(this);
|
||||||
|
|
@ -143,6 +148,27 @@ export class FirefoxLauncher implements ProductLauncher {
|
||||||
pipe,
|
pipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (protocol === 'webDriverBiDi') {
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
const connection = await runner.setupWebDriverBiDiConnection({
|
||||||
|
timeout,
|
||||||
|
slowMo,
|
||||||
|
preferredRevision: this._preferredRevision,
|
||||||
|
});
|
||||||
|
browser = await BiDiBrowser.create({
|
||||||
|
connection,
|
||||||
|
closeCallback: runner.close.bind(runner),
|
||||||
|
process: runner.proc,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
runner.kill();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
let browser;
|
let browser;
|
||||||
try {
|
try {
|
||||||
const connection = await runner.setupConnection({
|
const connection = await runner.setupConnection({
|
||||||
|
|
@ -151,7 +177,7 @@ export class FirefoxLauncher implements ProductLauncher {
|
||||||
slowMo,
|
slowMo,
|
||||||
preferredRevision: this._preferredRevision,
|
preferredRevision: this._preferredRevision,
|
||||||
});
|
});
|
||||||
browser = await Browser._create(
|
browser = await CDPBrowser._create(
|
||||||
this.product,
|
this.product,
|
||||||
connection,
|
connection,
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
import {Browser} from '../common/Browser.js';
|
import {Browser} from '../api/Browser.js';
|
||||||
import {BrowserFetcher} from './BrowserFetcher.js';
|
import {BrowserFetcher} from './BrowserFetcher.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
|
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
|
||||||
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js';
|
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js';
|
||||||
import {BrowserConnectOptions} from '../common/BrowserConnector.js';
|
import {BrowserConnectOptions} from '../common/BrowserConnector.js';
|
||||||
import {Browser} from '../common/Browser.js';
|
import {Browser} from '../api/Browser.js';
|
||||||
import {createLauncher, ProductLauncher} from './ProductLauncher.js';
|
import {createLauncher, ProductLauncher} from './ProductLauncher.js';
|
||||||
import {PUPPETEER_REVISIONS} from '../revisions.js';
|
import {PUPPETEER_REVISIONS} from '../revisions.js';
|
||||||
import {Product} from '../common/Product.js';
|
import {Product} from '../common/Product.js';
|
||||||
|
|
@ -78,6 +78,9 @@ export class PuppeteerNode extends Puppeteer {
|
||||||
#projectRoot?: string;
|
#projectRoot?: string;
|
||||||
#productName?: Product;
|
#productName?: Product;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
_preferredRevision: string;
|
_preferredRevision: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
/**
|
||||||
|
* CommonJS JavaScript code that provides the puppeteer utilities. See the
|
||||||
declare global {
|
* [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md)
|
||||||
const InjectedUtil: {
|
* for injection for more information.
|
||||||
createDeferredPromise: typeof createDeferredPromise;
|
*
|
||||||
};
|
* @internal
|
||||||
}
|
*/
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const source = SOURCE_CODE;
|
export const source = SOURCE_CODE;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,5 @@
|
||||||
"references": [
|
"references": [
|
||||||
{"path": "../vendor/tsconfig.cjs.json"},
|
{"path": "../vendor/tsconfig.cjs.json"},
|
||||||
{"path": "../compat/cjs/tsconfig.json"}
|
{"path": "../compat/cjs/tsconfig.json"}
|
||||||
],
|
]
|
||||||
"exclude": ["injected/injected.ts"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,5 @@
|
||||||
"references": [
|
"references": [
|
||||||
{"path": "../vendor/tsconfig.esm.json"},
|
{"path": "../vendor/tsconfig.esm.json"},
|
||||||
{"path": "../compat/esm/tsconfig.json"}
|
{"path": "../compat/esm/tsconfig.json"}
|
||||||
],
|
]
|
||||||
"exclude": ["injected/injected.ts"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// AUTOGENERATED - Use `npm run generate:sources` to regenerate.
|
// AUTOGENERATED - Use `npm run generate:sources` to regenerate.
|
||||||
|
|
||||||
|
export * from './api/Browser.js';
|
||||||
export * from './common/Accessibility.js';
|
export * from './common/Accessibility.js';
|
||||||
export * from './common/AriaQueryHandler.js';
|
export * from './common/AriaQueryHandler.js';
|
||||||
export * from './common/Browser.js';
|
export * from './common/Browser.js';
|
||||||
|
|
@ -23,11 +24,13 @@ export * from './common/FileChooser.js';
|
||||||
export * from './common/FirefoxTargetManager.js';
|
export * from './common/FirefoxTargetManager.js';
|
||||||
export * from './common/Frame.js';
|
export * from './common/Frame.js';
|
||||||
export * from './common/FrameManager.js';
|
export * from './common/FrameManager.js';
|
||||||
|
export * from './common/FrameTree.js';
|
||||||
export * from './common/HTTPRequest.js';
|
export * from './common/HTTPRequest.js';
|
||||||
export * from './common/HTTPResponse.js';
|
export * from './common/HTTPResponse.js';
|
||||||
export * from './common/Input.js';
|
export * from './common/Input.js';
|
||||||
export * from './common/IsolatedWorld.js';
|
export * from './common/IsolatedWorld.js';
|
||||||
export * from './common/JSHandle.js';
|
export * from './common/JSHandle.js';
|
||||||
|
export * from './common/LazyArg.js';
|
||||||
export * from './common/LifecycleWatcher.js';
|
export * from './common/LifecycleWatcher.js';
|
||||||
export * from './common/NetworkConditions.js';
|
export * from './common/NetworkConditions.js';
|
||||||
export * from './common/NetworkEventManager.js';
|
export * from './common/NetworkEventManager.js';
|
||||||
|
|
@ -47,12 +50,12 @@ export * from './common/Tracing.js';
|
||||||
export * from './common/types.js';
|
export * from './common/types.js';
|
||||||
export * from './common/USKeyboardLayout.js';
|
export * from './common/USKeyboardLayout.js';
|
||||||
export * from './common/util.js';
|
export * from './common/util.js';
|
||||||
|
export * from './common/WaitTask.js';
|
||||||
export * from './common/WebWorker.js';
|
export * from './common/WebWorker.js';
|
||||||
export * from './compat.d.js';
|
export * from './compat.d.js';
|
||||||
export * from './constants.js';
|
export * from './constants.js';
|
||||||
export * from './environment.js';
|
export * from './environment.js';
|
||||||
export * from './generated/injected.js';
|
export * from './generated/injected.js';
|
||||||
export * from './generated/version.js';
|
|
||||||
export * from './initializePuppeteer.js';
|
export * from './initializePuppeteer.js';
|
||||||
export * from './node/BrowserFetcher.js';
|
export * from './node/BrowserFetcher.js';
|
||||||
export * from './node/BrowserRunner.js';
|
export * from './node/BrowserRunner.js';
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
|
||||||
export interface DeferredPromise<T> extends Promise<T> {
|
export interface DeferredPromise<T> extends Promise<T> {
|
||||||
finished: () => boolean;
|
finished: () => boolean;
|
||||||
resolved: () => boolean;
|
resolved: () => boolean;
|
||||||
resolve: (_: T) => void;
|
resolve: (value: T) => void;
|
||||||
reject: (_: Error) => void;
|
reject: (reason?: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
|
||||||
): DeferredPromise<T> {
|
): DeferredPromise<T> {
|
||||||
let isResolved = false;
|
let isResolved = false;
|
||||||
let isRejected = false;
|
let isRejected = false;
|
||||||
let resolver = (_: T): void => {};
|
let resolver: (value: T) => void;
|
||||||
let rejector = (_: Error) => {};
|
let rejector: (reason?: unknown) => void;
|
||||||
const taskPromise = new Promise<T>((resolve, reject) => {
|
const taskPromise = new Promise<T>((resolve, reject) => {
|
||||||
resolver = resolve;
|
resolver = resolve;
|
||||||
rejector = reject;
|
rejector = reject;
|
||||||
|
|
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
|
||||||
isResolved = true;
|
isResolved = true;
|
||||||
resolver(value);
|
resolver(value);
|
||||||
},
|
},
|
||||||
reject: (err: Error) => {
|
reject: (err?: unknown) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
isRejected = true;
|
isRejected = true;
|
||||||
rejector(err);
|
rejector(err);
|
||||||
|
|
|
||||||
3128
remote/test/puppeteer/test/TestExpectations.json
Normal file
3128
remote/test/puppeteer/test/TestExpectations.json
Normal file
File diff suppressed because it is too large
Load diff
54
remote/test/puppeteer/test/TestSuites.json
Normal file
54
remote/test/puppeteer/test/TestSuites.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"testSuites": [
|
||||||
|
{
|
||||||
|
"id": "chrome-headless",
|
||||||
|
"platforms": ["linux", "win32", "darwin"],
|
||||||
|
"parameters": ["chrome", "headless"],
|
||||||
|
"expectedLineCoverage": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chrome-headful",
|
||||||
|
"platforms": ["linux"],
|
||||||
|
"parameters": ["chrome", "headful"],
|
||||||
|
"expectedLineCoverage": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chrome-new-headless",
|
||||||
|
"platforms": ["linux"],
|
||||||
|
"parameters": ["chrome", "chrome-headless"],
|
||||||
|
"expectedLineCoverage": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firefox-headless",
|
||||||
|
"platforms": ["linux"],
|
||||||
|
"parameters": ["firefox", "headless"],
|
||||||
|
"expectedLineCoverage": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firefox-bidi",
|
||||||
|
"platforms": ["linux"],
|
||||||
|
"parameters": ["firefox", "headless", "webDriverBiDi"],
|
||||||
|
"expectedLineCoverage": 56
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameterDefinitons": {
|
||||||
|
"chrome": {
|
||||||
|
"PUPPETEER_PRODUCT": "chrome"
|
||||||
|
},
|
||||||
|
"firefox": {
|
||||||
|
"PUPPETEER_PRODUCT": "firefox"
|
||||||
|
},
|
||||||
|
"headless": {
|
||||||
|
"HEADLESS": "true"
|
||||||
|
},
|
||||||
|
"headful": {
|
||||||
|
"HEADLESS": "false"
|
||||||
|
},
|
||||||
|
"chrome-headless": {
|
||||||
|
"HEADLESS": "chrome"
|
||||||
|
},
|
||||||
|
"webDriverBiDi": {
|
||||||
|
"PUPPETEER_PROTOCOL": "webDriverBiDi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
|
|
@ -20,11 +20,10 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||||
|
|
||||||
describeChromeOnly('Target.createCDPSession', function () {
|
describe('Target.createCDPSession', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {describeChromeOnly} from './mocha-utils.js';
|
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {
|
import {
|
||||||
NetworkManager,
|
NetworkManager,
|
||||||
|
|
@ -28,9 +26,16 @@ import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
||||||
|
|
||||||
class MockCDPSession extends EventEmitter {
|
class MockCDPSession extends EventEmitter {
|
||||||
async send(): Promise<any> {}
|
async send(): Promise<any> {}
|
||||||
|
connection() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
async detach() {}
|
||||||
|
id() {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describeChromeOnly('NetworkManager', () => {
|
describe('NetworkManager', () => {
|
||||||
it('should process extra info on multiple redirects', async () => {
|
it('should process extra info on multiple redirects', async () => {
|
||||||
const mockCDPSession = new MockCDPSession();
|
const mockCDPSession = new MockCDPSession();
|
||||||
new NetworkManager(mockCDPSession, true, {
|
new NetworkManager(mockCDPSession, true, {
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,24 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {describeChromeOnly, getTestState} from './mocha-utils'; // eslint-disable-line import/extensions
|
import {getTestState} from './mocha-utils'; // eslint-disable-line import/extensions
|
||||||
import utils from './utils.js';
|
import utils from './utils.js';
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Browser,
|
CDPBrowser,
|
||||||
BrowserContext,
|
CDPBrowserContext,
|
||||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
||||||
|
|
||||||
describeChromeOnly('TargetManager', () => {
|
describe('TargetManager', () => {
|
||||||
/* We use a special browser for this test as we need the --site-per-process flag */
|
/* We use a special browser for this test as we need the --site-per-process flag */
|
||||||
let browser: Browser;
|
let browser: CDPBrowser;
|
||||||
let context: BrowserContext;
|
let context: CDPBrowserContext;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
browser = await puppeteer.launch(
|
browser = (await puppeteer.launch(
|
||||||
Object.assign({}, defaultBrowserOptions, {
|
Object.assign({}, defaultBrowserOptions, {
|
||||||
args: (defaultBrowserOptions.args || []).concat([
|
args: (defaultBrowserOptions.args || []).concat([
|
||||||
'--site-per-process',
|
'--site-per-process',
|
||||||
|
|
@ -39,7 +39,7 @@ describeChromeOnly('TargetManager', () => {
|
||||||
'--host-rules=MAP * 127.0.0.1',
|
'--host-rules=MAP * 127.0.0.1',
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
);
|
)) as CDPBrowser;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,9 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
describeFailsFirefox,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describeFailsFirefox('Accessibility', function () {
|
describe('Accessibility', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
|
@ -346,7 +345,7 @@ describeFailsFirefox('Accessibility', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Firefox does not support contenteditable="plaintext-only".
|
// Firefox does not support contenteditable="plaintext-only".
|
||||||
describeFailsFirefox('plaintext contenteditable', function () {
|
describe('plaintext contenteditable', function () {
|
||||||
it('plain text field with role should not have children', async () => {
|
it('plain text field with role should not have children', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,13 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js';
|
import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js';
|
||||||
import utils from './utils.js';
|
import utils from './utils.js';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
describeChromeOnly('AriaQueryHandler', () => {
|
describe('AriaQueryHandler', () => {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
|
@ -447,7 +446,7 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
|
|
||||||
let divHidden = false;
|
let divHidden = false;
|
||||||
await page.setContent(
|
await page.setContent(
|
||||||
`<div role='button' style='display: block;'></div>`
|
`<div role='button' style='display: block;'>text</div>`
|
||||||
);
|
);
|
||||||
const waitForSelector = page
|
const waitForSelector = page
|
||||||
.waitForSelector('aria/[role="button"]', {hidden: true})
|
.waitForSelector('aria/[role="button"]', {hidden: true})
|
||||||
|
|
@ -469,7 +468,9 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divHidden = false;
|
let divHidden = false;
|
||||||
await page.setContent(`<div role='main' style='display: block;'></div>`);
|
await page.setContent(
|
||||||
|
`<div role='main' style='display: block;'>text</div>`
|
||||||
|
);
|
||||||
const waitForSelector = page
|
const waitForSelector = page
|
||||||
.waitForSelector('aria/[role="main"]', {hidden: true})
|
.waitForSelector('aria/[role="main"]', {hidden: true})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
@ -489,7 +490,7 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
it('hidden should wait for removal', async () => {
|
it('hidden should wait for removal', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
await page.setContent(`<div role='main'></div>`);
|
await page.setContent(`<div role='main'>text</div>`);
|
||||||
let divRemoved = false;
|
let divRemoved = false;
|
||||||
const waitForSelector = page
|
const waitForSelector = page
|
||||||
.waitForSelector('aria/[role="main"]', {hidden: true})
|
.waitForSelector('aria/[role="main"]', {hidden: true})
|
||||||
|
|
@ -517,15 +518,15 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
it('should respect timeout', async () => {
|
it('should respect timeout', async () => {
|
||||||
const {page, puppeteer} = getTestState();
|
const {page, puppeteer} = getTestState();
|
||||||
|
|
||||||
let error!: Error;
|
const error = await page
|
||||||
await page
|
.waitForSelector('aria/[role="button"]', {
|
||||||
.waitForSelector('aria/[role="button"]', {timeout: 10})
|
timeout: 10,
|
||||||
.catch(error_ => {
|
})
|
||||||
return (error = error_);
|
.catch(error => {
|
||||||
|
return error;
|
||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
'waiting for selector `[role="button"]` failed: timeout'
|
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
|
|
@ -533,17 +534,15 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
await page.setContent(`<div role='main'></div>`);
|
await page.setContent(`<div role='main'>text</div>`);
|
||||||
let error!: Error;
|
const promise = page.waitForSelector('aria/[role="main"]', {
|
||||||
await page
|
hidden: true,
|
||||||
.waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10})
|
timeout: 10,
|
||||||
.catch(error_ => {
|
});
|
||||||
return (error = error_);
|
await expect(promise).rejects.toMatchObject({
|
||||||
});
|
message:
|
||||||
expect(error).toBeTruthy();
|
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
|
||||||
expect(error.message).toContain(
|
});
|
||||||
'waiting for selector `[role="main"]` to be hidden failed: timeout'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respond to node attribute mutation', async () => {
|
it('should respond to node attribute mutation', async () => {
|
||||||
|
|
@ -582,7 +581,9 @@ describeChromeOnly('AriaQueryHandler', () => {
|
||||||
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
|
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error!.stack).toContain('waiting for selector `zombo` failed');
|
expect(error!.stack).toContain(
|
||||||
|
'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
59
remote/test/puppeteer/test/src/bidi/Connection.spec.ts
Normal file
59
remote/test/puppeteer/test/src/bidi/Connection.spec.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2022 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 expect from 'expect';
|
||||||
|
import {Connection} from '../../../lib/cjs/puppeteer/common/bidi/Connection.js';
|
||||||
|
import {ConnectionTransport} from '../../../lib/cjs/puppeteer/common/ConnectionTransport.js';
|
||||||
|
|
||||||
|
describe('WebDriver BiDi', () => {
|
||||||
|
describe('Connection', () => {
|
||||||
|
class TestConnectionTransport implements ConnectionTransport {
|
||||||
|
sent: string[] = [];
|
||||||
|
closed = false;
|
||||||
|
|
||||||
|
send(message: string) {
|
||||||
|
this.sent.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should work', async () => {
|
||||||
|
const transport = new TestConnectionTransport();
|
||||||
|
const connection = new Connection(transport);
|
||||||
|
const responsePromise = connection.send('session.status', {
|
||||||
|
context: 'context',
|
||||||
|
});
|
||||||
|
expect(transport.sent).toEqual([
|
||||||
|
`{"id":1,"method":"session.status","params":{"context":"context"}}`,
|
||||||
|
]);
|
||||||
|
const id = JSON.parse(transport.sent[0]!).id;
|
||||||
|
const rawResponse = {
|
||||||
|
id,
|
||||||
|
result: {ready: false, message: 'already connected'},
|
||||||
|
};
|
||||||
|
(transport as ConnectionTransport).onmessage?.(
|
||||||
|
JSON.stringify(rawResponse)
|
||||||
|
);
|
||||||
|
const response = await responsePromise;
|
||||||
|
expect(response).toEqual(rawResponse.result);
|
||||||
|
connection.dispose();
|
||||||
|
expect(transport.closed).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,10 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {
|
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||||
getTestState,
|
|
||||||
setupTestBrowserHooks,
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
import {waitEvent} from './utils.js';
|
import {waitEvent} from './utils.js';
|
||||||
|
|
||||||
describe('BrowserContext', function () {
|
describe('BrowserContext', function () {
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,9 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describeChromeOnly('Chromium-Specific Launcher tests', function () {
|
describe('Chromium-Specific Launcher tests', function () {
|
||||||
describe('Puppeteer.launch |browserURL| option', function () {
|
describe('Puppeteer.launch |browserURL| option', function () {
|
||||||
it('should be able to connect using browserUrl, with and without trailing slash', async () => {
|
it('should be able to connect using browserUrl, with and without trailing slash', async () => {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
|
|
@ -138,7 +137,7 @@ describeChromeOnly('Chromium-Specific Launcher tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeChromeOnly('Chromium-Specific Page Tests', function () {
|
describe('Chromium-Specific Page Tests', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
it('Page.setRequestInterception should work with intervention headers', async () => {
|
it('Page.setRequestInterception should work with intervention headers', async () => {
|
||||||
|
|
|
||||||
|
|
@ -51,24 +51,21 @@ describe('Page.click', function () {
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
it(
|
it('should click the button if window.Node is removed', async () => {
|
||||||
'should click the button if window.Node is removed',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/input/button.html');
|
await page.goto(server.PREFIX + '/input/button.html');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// @ts-expect-error Expected.
|
||||||
|
return delete window.Node;
|
||||||
|
});
|
||||||
|
await page.click('button');
|
||||||
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
// @ts-expect-error Expected.
|
return (globalThis as any).result;
|
||||||
return delete window.Node;
|
})
|
||||||
});
|
).toBe('Clicked');
|
||||||
await page.click('button');
|
});
|
||||||
expect(
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return (globalThis as any).result;
|
|
||||||
})
|
|
||||||
).toBe('Clicked');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4281
|
// @see https://github.com/puppeteer/puppeteer/issues/4281
|
||||||
it('should click on a span with an inline element inside', async () => {
|
it('should click on a span with an inline element inside', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
@ -421,7 +418,7 @@ describe('Page.click', function () {
|
||||||
).toBe('Clicked');
|
).toBe('Clicked');
|
||||||
});
|
});
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4110
|
// @see https://github.com/puppeteer/puppeteer/issues/4110
|
||||||
xit('should click the button with fixed position inside an iframe', async () => {
|
it.skip('should click the button with fixed position inside an iframe', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
|
||||||
|
|
@ -364,22 +364,19 @@ describe('Cookie specs', () => {
|
||||||
'At least one of the url and domain needs to be specified'
|
'At least one of the url and domain needs to be specified'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it(
|
it('should default to setting secure cookie for HTTPS websites', async () => {
|
||||||
'should default to setting secure cookie for HTTPS websites',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const SECURE_URL = 'https://example.com';
|
const SECURE_URL = 'https://example.com';
|
||||||
await page.setCookie({
|
await page.setCookie({
|
||||||
url: SECURE_URL,
|
url: SECURE_URL,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
value: 'bar',
|
value: 'bar',
|
||||||
});
|
});
|
||||||
const [cookie] = await page.cookies(SECURE_URL);
|
const [cookie] = await page.cookies(SECURE_URL);
|
||||||
expect(cookie!.secure).toBe(true);
|
expect(cookie!.secure).toBe(true);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should be able to set unsecure cookie for HTTP website', async () => {
|
it('should be able to set unsecure cookie for HTTP website', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -481,67 +478,64 @@ describe('Cookie specs', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it(
|
it('should set secure same-site cookies from a frame', async () => {
|
||||||
'should set secure same-site cookies from a frame',
|
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
async () => {
|
|
||||||
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
|
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
...defaultBrowserOptions,
|
...defaultBrowserOptions,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(httpsServer.PREFIX + '/grid.html');
|
||||||
|
await page.evaluate(src => {
|
||||||
|
let fulfill!: () => void;
|
||||||
|
const promise = new Promise<void>(x => {
|
||||||
|
return (fulfill = x);
|
||||||
|
});
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
iframe.onload = fulfill;
|
||||||
|
iframe.src = src;
|
||||||
|
return promise;
|
||||||
|
}, httpsServer.CROSS_PROCESS_PREFIX);
|
||||||
|
await page.setCookie({
|
||||||
|
name: '127-same-site-cookie',
|
||||||
|
value: 'best',
|
||||||
|
url: httpsServer.CROSS_PROCESS_PREFIX,
|
||||||
|
sameSite: 'None',
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = await browser.newPage();
|
expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
|
||||||
|
'127-same-site-cookie=best'
|
||||||
try {
|
);
|
||||||
await page.goto(httpsServer.PREFIX + '/grid.html');
|
expectCookieEquals(
|
||||||
await page.evaluate(src => {
|
await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
|
||||||
let fulfill!: () => void;
|
[
|
||||||
const promise = new Promise<void>(x => {
|
{
|
||||||
return (fulfill = x);
|
name: '127-same-site-cookie',
|
||||||
});
|
value: 'best',
|
||||||
const iframe = document.createElement('iframe');
|
domain: '127.0.0.1',
|
||||||
document.body.appendChild(iframe);
|
path: '/',
|
||||||
iframe.onload = fulfill;
|
sameParty: false,
|
||||||
iframe.src = src;
|
expires: -1,
|
||||||
return promise;
|
size: 24,
|
||||||
}, httpsServer.CROSS_PROCESS_PREFIX);
|
httpOnly: false,
|
||||||
await page.setCookie({
|
sameSite: 'None',
|
||||||
name: '127-same-site-cookie',
|
secure: true,
|
||||||
value: 'best',
|
session: true,
|
||||||
url: httpsServer.CROSS_PROCESS_PREFIX,
|
sourcePort: 443,
|
||||||
sameSite: 'None',
|
sourceScheme: 'Secure',
|
||||||
});
|
},
|
||||||
|
]
|
||||||
expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
|
);
|
||||||
'127-same-site-cookie=best'
|
} finally {
|
||||||
);
|
await page.close();
|
||||||
expectCookieEquals(
|
await browser.close();
|
||||||
await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: '127-same-site-cookie',
|
|
||||||
value: 'best',
|
|
||||||
domain: '127.0.0.1',
|
|
||||||
path: '/',
|
|
||||||
sameParty: false,
|
|
||||||
expires: -1,
|
|
||||||
size: 24,
|
|
||||||
httpOnly: false,
|
|
||||||
sameSite: 'None',
|
|
||||||
secure: true,
|
|
||||||
session: true,
|
|
||||||
sourcePort: 443,
|
|
||||||
sourceScheme: 'Secure',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await page.close();
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Page.deleteCookie', function () {
|
describe('Page.deleteCookie', function () {
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,10 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describe('Coverage specs', function () {
|
describe('Coverage specs', function () {
|
||||||
describeChromeOnly('JSCoverage', function () {
|
describe('JSCoverage', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
|
@ -134,7 +133,7 @@ describe('Coverage specs', function () {
|
||||||
).toBeGolden('jscoverage-involved.txt');
|
).toBeGolden('jscoverage-involved.txt');
|
||||||
});
|
});
|
||||||
// @see https://crbug.com/990945
|
// @see https://crbug.com/990945
|
||||||
xit('should not hang when there is a debugger statement', async () => {
|
it.skip('should not hang when there is a debugger statement', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
await page.coverage.startJSCoverage();
|
await page.coverage.startJSCoverage();
|
||||||
|
|
@ -190,7 +189,7 @@ describe('Coverage specs', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// @see https://crbug.com/990945
|
// @see https://crbug.com/990945
|
||||||
xit('should not hang when there is a debugger statement', async () => {
|
it.skip('should not hang when there is a debugger statement', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
await page.coverage.startJSCoverage();
|
await page.coverage.startJSCoverage();
|
||||||
|
|
@ -202,7 +201,7 @@ describe('Coverage specs', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeChromeOnly('CSSCoverage', function () {
|
describe('CSSCoverage', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,9 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describeChromeOnly('Input.drag', function () {
|
describe('Input.drag', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
it('should throw an exception if not enabled before usage', async () => {
|
it('should throw an exception if not enabled before usage', async () => {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ describe('Evaluation specs', function () {
|
||||||
});
|
});
|
||||||
expect(result).toBe(21);
|
expect(result).toBe(21);
|
||||||
});
|
});
|
||||||
(bigint ? it : xit)('should transfer BigInt', async () => {
|
(bigint ? it : it.skip)('should transfer BigInt', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
const result = await page.evaluate((a: bigint) => {
|
const result = await page.evaluate((a: bigint) => {
|
||||||
|
|
@ -113,18 +113,15 @@ describe('Evaluation specs', function () {
|
||||||
await page.goto(server.PREFIX + '/global-var.html');
|
await page.goto(server.PREFIX + '/global-var.html');
|
||||||
expect(await page.evaluate('globalVar')).toBe(123);
|
expect(await page.evaluate('globalVar')).toBe(123);
|
||||||
});
|
});
|
||||||
it(
|
it('should return undefined for objects with symbols', async () => {
|
||||||
'should return undefined for objects with symbols',
|
const {page} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return [Symbol('foo4')];
|
return [Symbol('foo4')];
|
||||||
})
|
})
|
||||||
).toBe(undefined);
|
).toBe(undefined);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should work with function shorthands', async () => {
|
it('should work with function shorthands', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -261,7 +258,7 @@ describe('Evaluation specs', function () {
|
||||||
expect(result).not.toBe(object);
|
expect(result).not.toBe(object);
|
||||||
expect(result).toEqual(object);
|
expect(result).toEqual(object);
|
||||||
});
|
});
|
||||||
(bigint ? it : xit)('should return BigInt', async () => {
|
(bigint ? it : it.skip)('should return BigInt', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
const result = await page.evaluate(() => {
|
const result = await page.evaluate(() => {
|
||||||
|
|
@ -322,18 +319,15 @@ describe('Evaluation specs', function () {
|
||||||
})
|
})
|
||||||
).toEqual({});
|
).toEqual({});
|
||||||
});
|
});
|
||||||
it(
|
it('should return undefined for non-serializable objects', async () => {
|
||||||
'should return undefined for non-serializable objects',
|
const {page} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return window;
|
return window;
|
||||||
})
|
})
|
||||||
).toBe(undefined);
|
).toBe(undefined);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should fail for circular object', async () => {
|
it('should fail for circular object', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -408,27 +402,24 @@ describe('Evaluation specs', function () {
|
||||||
});
|
});
|
||||||
expect(error.message).toContain('JSHandle is disposed');
|
expect(error.message).toContain('JSHandle is disposed');
|
||||||
});
|
});
|
||||||
it(
|
it('should throw if elementHandles are from other frames', async () => {
|
||||||
'should throw if elementHandles are from other frames',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
const bodyHandle = await page.frames()[1]!.$('body');
|
const bodyHandle = await page.frames()[1]!.$('body');
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page
|
await page
|
||||||
.evaluate(body => {
|
.evaluate(body => {
|
||||||
return body?.innerHTML;
|
return body?.innerHTML;
|
||||||
}, bodyHandle)
|
}, bodyHandle)
|
||||||
.catch(error_ => {
|
.catch(error_ => {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
'JSHandles can be evaluated only in the context they were created'
|
'JSHandles can be evaluated only in the context they were created'
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should simulate a user gesture', async () => {
|
it('should simulate a user gesture', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -459,19 +450,16 @@ describe('Evaluation specs', function () {
|
||||||
});
|
});
|
||||||
expect((error as Error).message).toContain('navigation');
|
expect((error as Error).message).toContain('navigation');
|
||||||
});
|
});
|
||||||
it(
|
it('should not throw an error when evaluation does a navigation', async () => {
|
||||||
'should not throw an error when evaluation does a navigation',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/one-style.html');
|
await page.goto(server.PREFIX + '/one-style.html');
|
||||||
const result = await page.evaluate(() => {
|
const result = await page.evaluate(() => {
|
||||||
(window as any).location = '/empty.html';
|
(window as any).location = '/empty.html';
|
||||||
return [42];
|
return [42];
|
||||||
});
|
});
|
||||||
expect(result).toEqual([42]);
|
expect(result).toEqual([42]);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should transfer 100Mb of data from page to node.js', async function () {
|
it('should transfer 100Mb of data from page to node.js', async function () {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {getTestState, itHeadlessOnly} from './mocha-utils.js';
|
import {getTestState} from './mocha-utils.js';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
describe('Fixtures', function () {
|
describe('Fixtures', function () {
|
||||||
itHeadlessOnly('dumpio option should work with pipe option', async () => {
|
it('dumpio option should work with pipe option', async () => {
|
||||||
const {defaultBrowserOptions, puppeteerPath, headless} = getTestState();
|
const {defaultBrowserOptions, puppeteerPath, headless} = getTestState();
|
||||||
if (headless === 'chrome') {
|
if (headless === 'chrome') {
|
||||||
// This test only works in the old headless mode.
|
// This test only works in the old headless mode.
|
||||||
|
|
|
||||||
|
|
@ -137,40 +137,37 @@ describe('Frame specs', function () {
|
||||||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it(
|
it('should send events when frames are manipulated dynamically', async () => {
|
||||||
'should send events when frames are manipulated dynamically',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
// validate frameattached events
|
// validate frameattached events
|
||||||
const attachedFrames: Frame[] = [];
|
const attachedFrames: Frame[] = [];
|
||||||
page.on('frameattached', frame => {
|
page.on('frameattached', frame => {
|
||||||
return attachedFrames.push(frame);
|
return attachedFrames.push(frame);
|
||||||
});
|
});
|
||||||
await utils.attachFrame(page, 'frame1', './assets/frame.html');
|
await utils.attachFrame(page, 'frame1', './assets/frame.html');
|
||||||
expect(attachedFrames.length).toBe(1);
|
expect(attachedFrames.length).toBe(1);
|
||||||
expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
|
expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
|
||||||
|
|
||||||
// validate framenavigated events
|
// validate framenavigated events
|
||||||
const navigatedFrames: Frame[] = [];
|
const navigatedFrames: Frame[] = [];
|
||||||
page.on('framenavigated', frame => {
|
page.on('framenavigated', frame => {
|
||||||
return navigatedFrames.push(frame);
|
return navigatedFrames.push(frame);
|
||||||
});
|
});
|
||||||
await utils.navigateFrame(page, 'frame1', './empty.html');
|
await utils.navigateFrame(page, 'frame1', './empty.html');
|
||||||
expect(navigatedFrames.length).toBe(1);
|
expect(navigatedFrames.length).toBe(1);
|
||||||
expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
|
expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||||
|
|
||||||
// validate framedetached events
|
// validate framedetached events
|
||||||
const detachedFrames: Frame[] = [];
|
const detachedFrames: Frame[] = [];
|
||||||
page.on('framedetached', frame => {
|
page.on('framedetached', frame => {
|
||||||
return detachedFrames.push(frame);
|
return detachedFrames.push(frame);
|
||||||
});
|
});
|
||||||
await utils.detachFrame(page, 'frame1');
|
await utils.detachFrame(page, 'frame1');
|
||||||
expect(detachedFrames.length).toBe(1);
|
expect(detachedFrames.length).toBe(1);
|
||||||
expect(detachedFrames[0]!.isDetached()).toBe(true);
|
expect(detachedFrames[0]!.isDetached()).toBe(true);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should send "framenavigated" when navigating on anchor URLs', async () => {
|
it('should send "framenavigated" when navigating on anchor URLs', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -299,31 +296,24 @@ describe('Frame specs', function () {
|
||||||
expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
|
expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
|
||||||
expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
|
expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
|
||||||
});
|
});
|
||||||
it(
|
it('should report different frame instance when frame re-attaches', async () => {
|
||||||
'should report different frame instance when frame re-attaches',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
const frame1 = await utils.attachFrame(
|
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
page,
|
await page.evaluate(() => {
|
||||||
'frame1',
|
(globalThis as any).frame = document.querySelector('#frame1');
|
||||||
server.EMPTY_PAGE
|
(globalThis as any).frame.remove();
|
||||||
);
|
});
|
||||||
await page.evaluate(() => {
|
expect(frame1!.isDetached()).toBe(true);
|
||||||
(globalThis as any).frame = document.querySelector('#frame1');
|
const [frame2] = await Promise.all([
|
||||||
(globalThis as any).frame.remove();
|
utils.waitEvent(page, 'frameattached'),
|
||||||
});
|
page.evaluate(() => {
|
||||||
expect(frame1!.isDetached()).toBe(true);
|
return document.body.appendChild((globalThis as any).frame);
|
||||||
const [frame2] = await Promise.all([
|
}),
|
||||||
utils.waitEvent(page, 'frameattached'),
|
]);
|
||||||
page.evaluate(() => {
|
expect(frame2.isDetached()).toBe(false);
|
||||||
return document.body.appendChild((globalThis as any).frame);
|
expect(frame1).not.toBe(frame2);
|
||||||
}),
|
});
|
||||||
]);
|
|
||||||
expect(frame2.isDetached()).toBe(false);
|
|
||||||
expect(frame1).not.toBe(frame2);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
it('should support url fragment', async () => {
|
it('should support url fragment', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,7 @@ import {
|
||||||
PuppeteerLaunchOptions,
|
PuppeteerLaunchOptions,
|
||||||
PuppeteerNode,
|
PuppeteerNode,
|
||||||
} from '../../lib/cjs/puppeteer/node/Puppeteer.js';
|
} from '../../lib/cjs/puppeteer/node/Puppeteer.js';
|
||||||
import {
|
import {getTestState} from './mocha-utils.js';
|
||||||
describeChromeOnly,
|
|
||||||
getTestState,
|
|
||||||
itFailsWindows,
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
|
|
||||||
const rmAsync = promisify(rimraf);
|
const rmAsync = promisify(rimraf);
|
||||||
const mkdtempAsync = promisify(fs.mkdtemp);
|
const mkdtempAsync = promisify(fs.mkdtemp);
|
||||||
|
|
@ -44,7 +40,7 @@ const serviceWorkerExtensionPath = path.join(
|
||||||
'extension'
|
'extension'
|
||||||
);
|
);
|
||||||
|
|
||||||
describeChromeOnly('headful tests', function () {
|
describe('headful tests', function () {
|
||||||
/* These tests fire up an actual browser so let's
|
/* These tests fire up an actual browser so let's
|
||||||
* allow a higher timeout
|
* allow a higher timeout
|
||||||
*/
|
*/
|
||||||
|
|
@ -214,43 +210,40 @@ describeChromeOnly('headful tests', function () {
|
||||||
expect(pages).toEqual(['about:blank']);
|
expect(pages).toEqual(['about:blank']);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
itFailsWindows(
|
it('headless should be able to read cookies written by headful', async () => {
|
||||||
'headless should be able to read cookies written by headful',
|
/* Needs investigation into why but this fails consistently on Windows CI. */
|
||||||
async () => {
|
const {server, puppeteer} = getTestState();
|
||||||
/* Needs investigation into why but this fails consistently on Windows CI. */
|
|
||||||
const {server, puppeteer} = getTestState();
|
|
||||||
|
|
||||||
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||||
// Write a cookie in headful chrome
|
// Write a cookie in headful chrome
|
||||||
const headfulBrowser = await launchBrowser(
|
const headfulBrowser = await launchBrowser(
|
||||||
puppeteer,
|
puppeteer,
|
||||||
Object.assign({userDataDir}, headfulOptions)
|
Object.assign({userDataDir}, headfulOptions)
|
||||||
);
|
);
|
||||||
const headfulPage = await headfulBrowser.newPage();
|
const headfulPage = await headfulBrowser.newPage();
|
||||||
await headfulPage.goto(server.EMPTY_PAGE);
|
await headfulPage.goto(server.EMPTY_PAGE);
|
||||||
await headfulPage.evaluate(() => {
|
await headfulPage.evaluate(() => {
|
||||||
return (document.cookie =
|
return (document.cookie =
|
||||||
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
|
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
|
||||||
});
|
});
|
||||||
await headfulBrowser.close();
|
await headfulBrowser.close();
|
||||||
// Read the cookie from headless chrome
|
// Read the cookie from headless chrome
|
||||||
const headlessBrowser = await launchBrowser(
|
const headlessBrowser = await launchBrowser(
|
||||||
puppeteer,
|
puppeteer,
|
||||||
Object.assign({userDataDir}, headlessOptions)
|
Object.assign({userDataDir}, headlessOptions)
|
||||||
);
|
);
|
||||||
const headlessPage = await headlessBrowser.newPage();
|
const headlessPage = await headlessBrowser.newPage();
|
||||||
await headlessPage.goto(server.EMPTY_PAGE);
|
await headlessPage.goto(server.EMPTY_PAGE);
|
||||||
const cookie = await headlessPage.evaluate(() => {
|
const cookie = await headlessPage.evaluate(() => {
|
||||||
return document.cookie;
|
return document.cookie;
|
||||||
});
|
});
|
||||||
await headlessBrowser.close();
|
await headlessBrowser.close();
|
||||||
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
||||||
await rmAsync(userDataDir).catch(() => {});
|
await rmAsync(userDataDir).catch(() => {});
|
||||||
expect(cookie).toBe('foo=true');
|
expect(cookie).toBe('foo=true');
|
||||||
}
|
});
|
||||||
);
|
|
||||||
// TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
|
// TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
|
||||||
xit('OOPIF: should report google.com frame', async () => {
|
it.skip('OOPIF: should report google.com frame', async () => {
|
||||||
const {server, puppeteer} = getTestState();
|
const {server, puppeteer} = getTestState();
|
||||||
|
|
||||||
// https://google.com is isolated by default in Chromium embedder.
|
// https://google.com is isolated by default in Chromium embedder.
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,10 @@
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {TLSSocket} from 'tls';
|
import {TLSSocket} from 'tls';
|
||||||
import {
|
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||||
Browser,
|
|
||||||
BrowserContext,
|
|
||||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
|
||||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
||||||
import {
|
import {getTestState} from './mocha-utils.js';
|
||||||
getTestState
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
|
|
||||||
describe('ignoreHTTPSErrors', function () {
|
describe('ignoreHTTPSErrors', function () {
|
||||||
/* Note that this test creates its own browser rather than use
|
/* Note that this test creates its own browser rather than use
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,35 @@ import {
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describe('InjectedUtil tests', function () {
|
describe('PuppeteerUtil tests', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
it('should work', async () => {
|
it('should work', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
const handle = await page
|
const world = page.mainFrame().worlds[PUPPETEER_WORLD];
|
||||||
.mainFrame()
|
const value = await world.evaluate(PuppeteerUtil => {
|
||||||
.worlds[PUPPETEER_WORLD].evaluate(() => {
|
return typeof PuppeteerUtil === 'object';
|
||||||
return typeof InjectedUtil === 'object';
|
}, world.puppeteerUtil);
|
||||||
});
|
expect(value).toBeTruthy();
|
||||||
expect(handle).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
describe('createFunction tests', function () {
|
||||||
|
it('should work', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
const world = page.mainFrame().worlds[PUPPETEER_WORLD];
|
||||||
|
const value = await world.evaluate(
|
||||||
|
({createFunction}, fnString) => {
|
||||||
|
return createFunction(fnString)(4);
|
||||||
|
},
|
||||||
|
await world.puppeteerUtil,
|
||||||
|
(() => {
|
||||||
|
return 4;
|
||||||
|
}).toString()
|
||||||
|
);
|
||||||
|
expect(value).toBe(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
describeFailsFirefox,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
|
const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
|
||||||
|
|
@ -29,7 +28,7 @@ describe('input tests', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
describeFailsFirefox('input', function () {
|
describe('input', function () {
|
||||||
it('should upload the file', async () => {
|
it('should upload the file', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -76,7 +75,7 @@ describe('input tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeFailsFirefox('Page.waitForFileChooser', function () {
|
describe('Page.waitForFileChooser', function () {
|
||||||
it('should work when file input is attached to DOM', async () => {
|
it('should work when file input is attached to DOM', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -159,7 +158,7 @@ describe('input tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeFailsFirefox('FileChooser.accept', function () {
|
describe('FileChooser.accept', function () {
|
||||||
it('should accept single file', async () => {
|
it('should accept single file', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -325,7 +324,7 @@ describe('input tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeFailsFirefox('FileChooser.cancel', function () {
|
describe('FileChooser.cancel', function () {
|
||||||
it('should cancel dialog', async () => {
|
it('should cancel dialog', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -373,7 +372,7 @@ describe('input tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeFailsFirefox('FileChooser.isMultiple', () => {
|
describe('FileChooser.isMultiple', () => {
|
||||||
it('should work for single file pick', async () => {
|
it('should work for single file pick', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,21 +119,18 @@ describe('Keyboard', function () {
|
||||||
})
|
})
|
||||||
).toBe('a');
|
).toBe('a');
|
||||||
});
|
});
|
||||||
it(
|
it('ElementHandle.press should support |text| option', async () => {
|
||||||
'ElementHandle.press should support |text| option',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||||
const textarea = (await page.$('textarea'))!;
|
const textarea = (await page.$('textarea'))!;
|
||||||
await textarea.press('a', {text: 'ё'});
|
await textarea.press('a', {text: 'ё'});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return document.querySelector('textarea')!.value;
|
return document.querySelector('textarea')!.value;
|
||||||
})
|
})
|
||||||
).toBe('ё');
|
).toBe('ё');
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should send a character with sendCharacter', async () => {
|
it('should send a character with sendCharacter', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,7 @@ import {TLSSocket} from 'tls';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
import {Product} from '../../lib/cjs/puppeteer/common/Product.js';
|
import {Product} from '../../lib/cjs/puppeteer/common/Product.js';
|
||||||
import {
|
import {getTestState, itOnlyRegularInstall} from './mocha-utils.js';
|
||||||
getTestState,
|
|
||||||
itChromeOnly,
|
|
||||||
itFirefoxOnly,
|
|
||||||
itOnlyRegularInstall,
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
import utils from './utils.js';
|
import utils from './utils.js';
|
||||||
|
|
||||||
const mkdtempAsync = promisify(fs.mkdtemp);
|
const mkdtempAsync = promisify(fs.mkdtemp);
|
||||||
|
|
@ -208,6 +203,11 @@ describe('Launcher specs', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Puppeteer.launch', function () {
|
describe('Puppeteer.launch', function () {
|
||||||
|
it('can launch and close the browser', async () => {
|
||||||
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
|
const browser = await puppeteer.launch(defaultBrowserOptions);
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
it('should reject all promises when browser is closed', async () => {
|
it('should reject all promises when browser is closed', async () => {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
const browser = await puppeteer.launch(defaultBrowserOptions);
|
const browser = await puppeteer.launch(defaultBrowserOptions);
|
||||||
|
|
@ -250,7 +250,7 @@ describe('Launcher specs', function () {
|
||||||
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
||||||
await rmAsync(userDataDir).catch(() => {});
|
await rmAsync(userDataDir).catch(() => {});
|
||||||
});
|
});
|
||||||
itChromeOnly('tmp profile should be cleaned up', async () => {
|
it('tmp profile should be cleaned up', async () => {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
|
|
||||||
// Set a custom test tmp dir so that we can validate that
|
// Set a custom test tmp dir so that we can validate that
|
||||||
|
|
@ -279,7 +279,7 @@ describe('Launcher specs', function () {
|
||||||
// Restore env var
|
// Restore env var
|
||||||
process.env['PUPPETEER_TMP_DIR'] = '';
|
process.env['PUPPETEER_TMP_DIR'] = '';
|
||||||
});
|
});
|
||||||
itFirefoxOnly('userDataDir option restores preferences', async () => {
|
it('userDataDir option restores preferences', async () => {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
|
|
||||||
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||||
|
|
@ -325,7 +325,7 @@ describe('Launcher specs', function () {
|
||||||
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
||||||
await rmAsync(userDataDir).catch(() => {});
|
await rmAsync(userDataDir).catch(() => {});
|
||||||
});
|
});
|
||||||
itChromeOnly('userDataDir argument with non-existent dir', async () => {
|
it('userDataDir argument with non-existent dir', async () => {
|
||||||
const {isChrome, puppeteer, defaultBrowserOptions} = getTestState();
|
const {isChrome, puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
|
|
||||||
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||||
|
|
@ -459,49 +459,43 @@ describe('Launcher specs', function () {
|
||||||
await page.close();
|
await page.close();
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
itChromeOnly(
|
it('should filter out ignored default arguments in Chrome', async () => {
|
||||||
'should filter out ignored default arguments in Chrome',
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
async () => {
|
// Make sure we launch with `--enable-automation` by default.
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const defaultArgs = puppeteer.defaultArgs();
|
||||||
// Make sure we launch with `--enable-automation` by default.
|
const browser = await puppeteer.launch(
|
||||||
const defaultArgs = puppeteer.defaultArgs();
|
Object.assign({}, defaultBrowserOptions, {
|
||||||
const browser = await puppeteer.launch(
|
// Ignore first and third default argument.
|
||||||
Object.assign({}, defaultBrowserOptions, {
|
ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
|
||||||
// Ignore first and third default argument.
|
})
|
||||||
ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
|
);
|
||||||
})
|
const spawnargs = browser.process()!.spawnargs;
|
||||||
);
|
if (!spawnargs) {
|
||||||
const spawnargs = browser.process()!.spawnargs;
|
throw new Error('spawnargs not present');
|
||||||
if (!spawnargs) {
|
|
||||||
throw new Error('spawnargs not present');
|
|
||||||
}
|
|
||||||
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
|
|
||||||
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
|
|
||||||
expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
|
|
||||||
await browser.close();
|
|
||||||
}
|
}
|
||||||
);
|
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
|
||||||
itFirefoxOnly(
|
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
|
||||||
'should filter out ignored default argument in Firefox',
|
expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
|
||||||
async () => {
|
await browser.close();
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
});
|
||||||
|
it('should filter out ignored default argument in Firefox', async () => {
|
||||||
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
|
|
||||||
const defaultArgs = puppeteer.defaultArgs();
|
const defaultArgs = puppeteer.defaultArgs();
|
||||||
const browser = await puppeteer.launch(
|
const browser = await puppeteer.launch(
|
||||||
Object.assign({}, defaultBrowserOptions, {
|
Object.assign({}, defaultBrowserOptions, {
|
||||||
// Only the first argument is fixed, others are optional.
|
// Only the first argument is fixed, others are optional.
|
||||||
ignoreDefaultArgs: [defaultArgs[0]!],
|
ignoreDefaultArgs: [defaultArgs[0]!],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const spawnargs = browser.process()!.spawnargs;
|
const spawnargs = browser.process()!.spawnargs;
|
||||||
if (!spawnargs) {
|
if (!spawnargs) {
|
||||||
throw new Error('spawnargs not present');
|
throw new Error('spawnargs not present');
|
||||||
}
|
|
||||||
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
|
|
||||||
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
|
|
||||||
await browser.close();
|
|
||||||
}
|
}
|
||||||
);
|
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
|
||||||
|
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
it('should have default URL when launching browser', async function () {
|
it('should have default URL when launching browser', async function () {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
const browser = await puppeteer.launch(defaultBrowserOptions);
|
const browser = await puppeteer.launch(defaultBrowserOptions);
|
||||||
|
|
@ -511,24 +505,21 @@ describe('Launcher specs', function () {
|
||||||
expect(pages).toEqual(['about:blank']);
|
expect(pages).toEqual(['about:blank']);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
it(
|
it('should have custom URL when launching browser', async () => {
|
||||||
'should have custom URL when launching browser',
|
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
async () => {
|
|
||||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
|
||||||
|
|
||||||
const options = Object.assign({}, defaultBrowserOptions);
|
const options = Object.assign({}, defaultBrowserOptions);
|
||||||
options.args = [server.EMPTY_PAGE].concat(options.args || []);
|
options.args = [server.EMPTY_PAGE].concat(options.args || []);
|
||||||
const browser = await puppeteer.launch(options);
|
const browser = await puppeteer.launch(options);
|
||||||
const pages = await browser.pages();
|
const pages = await browser.pages();
|
||||||
expect(pages.length).toBe(1);
|
expect(pages.length).toBe(1);
|
||||||
const page = pages[0]!;
|
const page = pages[0]!;
|
||||||
if (page.url() !== server.EMPTY_PAGE) {
|
if (page.url() !== server.EMPTY_PAGE) {
|
||||||
await page.waitForNavigation();
|
await page.waitForNavigation();
|
||||||
}
|
|
||||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
|
||||||
await browser.close();
|
|
||||||
}
|
}
|
||||||
);
|
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
it('should pass the timeout parameter to browser.waitForTarget', async () => {
|
it('should pass the timeout parameter to browser.waitForTarget', async () => {
|
||||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
const options = Object.assign({}, defaultBrowserOptions, {
|
const options = Object.assign({}, defaultBrowserOptions, {
|
||||||
|
|
@ -614,24 +605,21 @@ describe('Launcher specs', function () {
|
||||||
});
|
});
|
||||||
expect(error.message).toContain('either pipe or debugging port');
|
expect(error.message).toContain('either pipe or debugging port');
|
||||||
});
|
});
|
||||||
itChromeOnly(
|
it('should launch Chrome properly with --no-startup-window and waitForInitialPage=false', async () => {
|
||||||
'should launch Chrome properly with --no-startup-window and waitForInitialPage=false',
|
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||||
async () => {
|
const options = {
|
||||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
waitForInitialPage: false,
|
||||||
const options = {
|
// This is needed to prevent Puppeteer from adding an initial blank page.
|
||||||
waitForInitialPage: false,
|
// See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
|
||||||
// This is needed to prevent Puppeteer from adding an initial blank page.
|
ignoreDefaultArgs: true,
|
||||||
// See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
|
...defaultBrowserOptions,
|
||||||
ignoreDefaultArgs: true,
|
args: ['--no-startup-window'],
|
||||||
...defaultBrowserOptions,
|
};
|
||||||
args: ['--no-startup-window'],
|
const browser = await puppeteer.launch(options);
|
||||||
};
|
const pages = await browser.pages();
|
||||||
const browser = await puppeteer.launch(options);
|
expect(pages.length).toBe(0);
|
||||||
const pages = await browser.pages();
|
await browser.close();
|
||||||
expect(pages.length).toBe(0);
|
});
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Puppeteer.launch', function () {
|
describe('Puppeteer.launch', function () {
|
||||||
|
|
@ -808,68 +796,62 @@ describe('Launcher specs', function () {
|
||||||
.sort()
|
.sort()
|
||||||
).toEqual(['about:blank', server.EMPTY_PAGE]);
|
).toEqual(['about:blank', server.EMPTY_PAGE]);
|
||||||
});
|
});
|
||||||
it(
|
it('should be able to reconnect to a disconnected browser', async () => {
|
||||||
'should be able to reconnect to a disconnected browser',
|
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
async () => {
|
|
||||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
|
||||||
|
|
||||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||||
const page = await originalBrowser.newPage();
|
const page = await originalBrowser.newPage();
|
||||||
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
||||||
originalBrowser.disconnect();
|
originalBrowser.disconnect();
|
||||||
|
|
||||||
const browser = await puppeteer.connect({browserWSEndpoint});
|
const browser = await puppeteer.connect({browserWSEndpoint});
|
||||||
const pages = await browser.pages();
|
const pages = await browser.pages();
|
||||||
const restoredPage = pages.find(page => {
|
const restoredPage = pages.find(page => {
|
||||||
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
||||||
})!;
|
})!;
|
||||||
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
|
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
|
||||||
'http://localhost:<PORT>/frames/nested-frames.html',
|
'http://localhost:<PORT>/frames/nested-frames.html',
|
||||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||||
' http://localhost:<PORT>/frames/frame.html (dos)',
|
' http://localhost:<PORT>/frames/frame.html (dos)',
|
||||||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
await restoredPage.evaluate(() => {
|
await restoredPage.evaluate(() => {
|
||||||
return 7 * 8;
|
return 7 * 8;
|
||||||
})
|
})
|
||||||
).toBe(56);
|
).toBe(56);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
|
// @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
|
||||||
it(
|
it('should be able to connect to the same page simultaneously', async () => {
|
||||||
'should be able to connect to the same page simultaneously',
|
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
async () => {
|
|
||||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
|
||||||
|
|
||||||
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
||||||
const browserTwo = await puppeteer.connect({
|
const browserTwo = await puppeteer.connect({
|
||||||
browserWSEndpoint: browserOne.wsEndpoint(),
|
browserWSEndpoint: browserOne.wsEndpoint(),
|
||||||
});
|
});
|
||||||
const [page1, page2] = await Promise.all([
|
const [page1, page2] = await Promise.all([
|
||||||
new Promise<Page>(x => {
|
new Promise<Page>(x => {
|
||||||
return browserOne.once('targetcreated', target => {
|
return browserOne.once('targetcreated', target => {
|
||||||
return x(target.page());
|
return x(target.page());
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
browserTwo.newPage(),
|
browserTwo.newPage(),
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
await page1.evaluate(() => {
|
await page1.evaluate(() => {
|
||||||
return 7 * 8;
|
return 7 * 8;
|
||||||
})
|
})
|
||||||
).toBe(56);
|
).toBe(56);
|
||||||
expect(
|
expect(
|
||||||
await page2.evaluate(() => {
|
await page2.evaluate(() => {
|
||||||
return 7 * 6;
|
return 7 * 6;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
await browserOne.close();
|
await browserOne.close();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should be able to reconnect', async () => {
|
it('should be able to reconnect', async () => {
|
||||||
const {puppeteer, server, defaultBrowserOptions} = getTestState();
|
const {puppeteer, server, defaultBrowserOptions} = getTestState();
|
||||||
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
||||||
|
|
@ -932,7 +914,7 @@ describe('Launcher specs', function () {
|
||||||
|
|
||||||
describe('when the product is chrome, platform is not darwin, and arch is arm64', () => {
|
describe('when the product is chrome, platform is not darwin, and arch is arm64', () => {
|
||||||
describe('and the executable exists', () => {
|
describe('and the executable exists', () => {
|
||||||
itChromeOnly('returns /usr/bin/chromium-browser', async () => {
|
it('returns /usr/bin/chromium-browser', async () => {
|
||||||
const {puppeteer} = getTestState();
|
const {puppeteer} = getTestState();
|
||||||
const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
|
const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
|
||||||
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
||||||
|
|
@ -971,26 +953,21 @@ describe('Launcher specs', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('and the executable does not exist', () => {
|
describe('and the executable does not exist', () => {
|
||||||
itChromeOnly(
|
it('does not return /usr/bin/chromium-browser', async () => {
|
||||||
'does not return /usr/bin/chromium-browser',
|
const {puppeteer} = getTestState();
|
||||||
async () => {
|
const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
|
||||||
const {puppeteer} = getTestState();
|
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
||||||
const osPlatformStub = sinon
|
const fsExistsStub = sinon.stub(fs, 'existsSync');
|
||||||
.stub(os, 'platform')
|
fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false);
|
||||||
.returns('linux');
|
|
||||||
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
|
||||||
const fsExistsStub = sinon.stub(fs, 'existsSync');
|
|
||||||
fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false);
|
|
||||||
|
|
||||||
const executablePath = puppeteer.executablePath();
|
const executablePath = puppeteer.executablePath();
|
||||||
|
|
||||||
expect(executablePath).not.toEqual('/usr/bin/chromium-browser');
|
expect(executablePath).not.toEqual('/usr/bin/chromium-browser');
|
||||||
|
|
||||||
osPlatformStub.restore();
|
osPlatformStub.restore();
|
||||||
osArchStub.restore();
|
osArchStub.restore();
|
||||||
fsExistsStub.restore();
|
fsExistsStub.restore();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1020,51 +997,48 @@ describe('Launcher specs', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Browser.Events.disconnected', function () {
|
describe('Browser.Events.disconnected', function () {
|
||||||
it(
|
it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
|
||||||
'should be emitted when: browser gets closed, disconnected or underlying websocket gets closed',
|
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||||
async () => {
|
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
const remoteBrowser1 = await puppeteer.connect({
|
||||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
browserWSEndpoint,
|
||||||
const remoteBrowser1 = await puppeteer.connect({
|
});
|
||||||
browserWSEndpoint,
|
const remoteBrowser2 = await puppeteer.connect({
|
||||||
});
|
browserWSEndpoint,
|
||||||
const remoteBrowser2 = await puppeteer.connect({
|
});
|
||||||
browserWSEndpoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
let disconnectedOriginal = 0;
|
let disconnectedOriginal = 0;
|
||||||
let disconnectedRemote1 = 0;
|
let disconnectedRemote1 = 0;
|
||||||
let disconnectedRemote2 = 0;
|
let disconnectedRemote2 = 0;
|
||||||
originalBrowser.on('disconnected', () => {
|
originalBrowser.on('disconnected', () => {
|
||||||
return ++disconnectedOriginal;
|
return ++disconnectedOriginal;
|
||||||
});
|
});
|
||||||
remoteBrowser1.on('disconnected', () => {
|
remoteBrowser1.on('disconnected', () => {
|
||||||
return ++disconnectedRemote1;
|
return ++disconnectedRemote1;
|
||||||
});
|
});
|
||||||
remoteBrowser2.on('disconnected', () => {
|
remoteBrowser2.on('disconnected', () => {
|
||||||
return ++disconnectedRemote2;
|
return ++disconnectedRemote2;
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
utils.waitEvent(remoteBrowser2, 'disconnected'),
|
utils.waitEvent(remoteBrowser2, 'disconnected'),
|
||||||
remoteBrowser2.disconnect(),
|
remoteBrowser2.disconnect(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(disconnectedOriginal).toBe(0);
|
expect(disconnectedOriginal).toBe(0);
|
||||||
expect(disconnectedRemote1).toBe(0);
|
expect(disconnectedRemote1).toBe(0);
|
||||||
expect(disconnectedRemote2).toBe(1);
|
expect(disconnectedRemote2).toBe(1);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
utils.waitEvent(remoteBrowser1, 'disconnected'),
|
utils.waitEvent(remoteBrowser1, 'disconnected'),
|
||||||
utils.waitEvent(originalBrowser, 'disconnected'),
|
utils.waitEvent(originalBrowser, 'disconnected'),
|
||||||
originalBrowser.close(),
|
originalBrowser.close(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(disconnectedOriginal).toBe(1);
|
expect(disconnectedOriginal).toBe(1);
|
||||||
expect(disconnectedRemote1).toBe(1);
|
expect(disconnectedRemote1).toBe(1);
|
||||||
expect(disconnectedRemote2).toBe(1);
|
expect(disconnectedRemote2).toBe(1);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,10 @@
|
||||||
import Protocol from 'devtools-protocol';
|
import Protocol from 'devtools-protocol';
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import {
|
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||||
Browser,
|
|
||||||
BrowserContext,
|
|
||||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
|
||||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -34,6 +30,7 @@ import {
|
||||||
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
|
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
|
||||||
import {TestServer} from '../../utils/testserver/lib/index.js';
|
import {TestServer} from '../../utils/testserver/lib/index.js';
|
||||||
import {extendExpectWithToBeGolden} from './utils.js';
|
import {extendExpectWithToBeGolden} from './utils.js';
|
||||||
|
import * as Mocha from 'mocha';
|
||||||
|
|
||||||
const setupServer = async () => {
|
const setupServer = async () => {
|
||||||
const assetsPath = path.join(__dirname, '../assets');
|
const assetsPath = path.join(__dirname, '../assets');
|
||||||
|
|
@ -63,14 +60,15 @@ export const getTestState = (): PuppeteerTestState => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const product =
|
const product =
|
||||||
process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'Chromium';
|
process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome';
|
||||||
|
|
||||||
const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false;
|
const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false;
|
||||||
|
|
||||||
const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
|
const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
|
||||||
const isHeadless = headless === 'true' || headless === 'chrome';
|
const isHeadless = headless === 'true' || headless === 'chrome';
|
||||||
const isFirefox = product === 'firefox';
|
const isFirefox = product === 'firefox';
|
||||||
const isChrome = product === 'Chromium';
|
const isChrome = product === 'chrome';
|
||||||
|
const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp';
|
||||||
|
|
||||||
let extraLaunchOptions = {};
|
let extraLaunchOptions = {};
|
||||||
try {
|
try {
|
||||||
|
|
@ -91,6 +89,7 @@ const defaultBrowserOptions = Object.assign(
|
||||||
executablePath: process.env['BINARY'],
|
executablePath: process.env['BINARY'],
|
||||||
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
|
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
|
||||||
dumpio: !!process.env['DUMPIO'],
|
dumpio: !!process.env['DUMPIO'],
|
||||||
|
protocol: protocol as 'cdp' | 'webDriverBiDi',
|
||||||
},
|
},
|
||||||
extraLaunchOptions
|
extraLaunchOptions
|
||||||
);
|
);
|
||||||
|
|
@ -125,7 +124,11 @@ declare module 'expect/build/types' {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupGoldenAssertions = (): void => {
|
const setupGoldenAssertions = (): void => {
|
||||||
const suffix = product.toLowerCase();
|
let suffix = product.toLowerCase();
|
||||||
|
if (suffix === 'chrome') {
|
||||||
|
// TODO: to avoid moving golden folders.
|
||||||
|
suffix = 'chromium';
|
||||||
|
}
|
||||||
const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
|
const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
|
||||||
const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
|
const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
|
||||||
if (fs.existsSync(OUTPUT_DIR)) {
|
if (fs.existsSync(OUTPUT_DIR)) {
|
||||||
|
|
@ -152,116 +155,21 @@ interface PuppeteerTestState {
|
||||||
}
|
}
|
||||||
const state: Partial<PuppeteerTestState> = {};
|
const state: Partial<PuppeteerTestState> = {};
|
||||||
|
|
||||||
export const itFailsFirefox = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (isFirefox) {
|
|
||||||
return xit(description, body);
|
|
||||||
} else {
|
|
||||||
return it(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itChromeOnly = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (isChrome) {
|
|
||||||
return it(description, body);
|
|
||||||
} else {
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itHeadlessOnly = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (isChrome && isHeadless === true) {
|
|
||||||
return it(description, body);
|
|
||||||
} else {
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itHeadfulOnly = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (isChrome && isHeadless === false) {
|
|
||||||
return it(description, body);
|
|
||||||
} else {
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itFirefoxOnly = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (isFirefox) {
|
|
||||||
return it(description, body);
|
|
||||||
} else {
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itOnlyRegularInstall = (
|
export const itOnlyRegularInstall = (
|
||||||
description: string,
|
description: string,
|
||||||
body: Mocha.Func
|
body: Mocha.AsyncFunc
|
||||||
): Mocha.Test => {
|
): Mocha.Test => {
|
||||||
if (alternativeInstall || process.env['BINARY']) {
|
if (alternativeInstall || process.env['BINARY']) {
|
||||||
return xit(description, body);
|
return it.skip(description, body);
|
||||||
} else {
|
} else {
|
||||||
return it(description, body);
|
return it(description, body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const itFailsWindowsUntilDate = (
|
if (
|
||||||
date: Date,
|
process.env['MOCHA_WORKER_ID'] === undefined ||
|
||||||
description: string,
|
process.env['MOCHA_WORKER_ID'] === '0'
|
||||||
body: Mocha.Func
|
) {
|
||||||
): Mocha.Test => {
|
|
||||||
if (os.platform() === 'win32' && Date.now() < date.getTime()) {
|
|
||||||
// we are within the deferred time so skip the test
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
return it(description, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const itFailsWindows = (
|
|
||||||
description: string,
|
|
||||||
body: Mocha.Func
|
|
||||||
): Mocha.Test => {
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
return xit(description, body);
|
|
||||||
}
|
|
||||||
return it(description, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const describeFailsFirefox = (
|
|
||||||
description: string,
|
|
||||||
body: (this: Mocha.Suite) => void
|
|
||||||
): void | Mocha.Suite => {
|
|
||||||
if (isFirefox) {
|
|
||||||
return xdescribe(description, body);
|
|
||||||
} else {
|
|
||||||
return describe(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const describeChromeOnly = (
|
|
||||||
description: string,
|
|
||||||
body: (this: Mocha.Suite) => void
|
|
||||||
): Mocha.Suite | void => {
|
|
||||||
if (isChrome) {
|
|
||||||
return describe(description, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env['MOCHA_WORKER_ID'] === '0') {
|
|
||||||
console.log(
|
console.log(
|
||||||
`Running unit tests with:
|
`Running unit tests with:
|
||||||
-> product: ${product}
|
-> product: ${product}
|
||||||
|
|
@ -290,7 +198,7 @@ export const setupTestBrowserHooks = (): void => {
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await state.browser!.close();
|
await state.browser?.close();
|
||||||
state.browser = undefined;
|
state.browser = undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -302,7 +210,7 @@ export const setupTestPageAndContextHooks = (): void => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await state.context!.close();
|
await state.context?.close();
|
||||||
state.context = undefined;
|
state.context = undefined;
|
||||||
state.page = undefined;
|
state.page = undefined;
|
||||||
});
|
});
|
||||||
|
|
@ -387,3 +295,14 @@ export const shortWaitForArrayToHaveAtLeastNElements = async (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createTimeout = <T>(
|
||||||
|
n: number,
|
||||||
|
value?: T
|
||||||
|
): Promise<T | undefined> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
return resolve(value);
|
||||||
|
}, n);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -137,24 +137,21 @@ describe('Mouse', function () {
|
||||||
})
|
})
|
||||||
).toBe('button-91');
|
).toBe('button-91');
|
||||||
});
|
});
|
||||||
it(
|
it('should trigger hover state with removed window.Node', async () => {
|
||||||
'should trigger hover state with removed window.Node',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/input/scrollable.html');
|
await page.goto(server.PREFIX + '/input/scrollable.html');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// @ts-expect-error Expected.
|
||||||
|
return delete window.Node;
|
||||||
|
});
|
||||||
|
await page.hover('#button-6');
|
||||||
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
// @ts-expect-error Expected.
|
return document.querySelector('button:hover')!.id;
|
||||||
return delete window.Node;
|
})
|
||||||
});
|
).toBe('button-6');
|
||||||
await page.hover('#button-6');
|
});
|
||||||
expect(
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return document.querySelector('button:hover')!.id;
|
|
||||||
})
|
|
||||||
).toBe('button-6');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
it('should set modifier keys on click', async () => {
|
it('should set modifier keys on click', async () => {
|
||||||
const {page, server, isFirefox} = getTestState();
|
const {page, server, isFirefox} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,28 +122,22 @@ describe('navigation', function () {
|
||||||
const response = await page.goto(server.PREFIX + '/grid.html');
|
const response = await page.goto(server.PREFIX + '/grid.html');
|
||||||
expect(response!.status()).toBe(200);
|
expect(response!.status()).toBe(200);
|
||||||
});
|
});
|
||||||
it(
|
it('should navigate to empty page with networkidle0', async () => {
|
||||||
'should navigate to empty page with networkidle0',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
const response = await page.goto(server.EMPTY_PAGE, {
|
const response = await page.goto(server.EMPTY_PAGE, {
|
||||||
waitUntil: 'networkidle0',
|
waitUntil: 'networkidle0',
|
||||||
});
|
});
|
||||||
expect(response!.status()).toBe(200);
|
expect(response!.status()).toBe(200);
|
||||||
}
|
});
|
||||||
);
|
it('should navigate to empty page with networkidle2', async () => {
|
||||||
it(
|
const {page, server} = getTestState();
|
||||||
'should navigate to empty page with networkidle2',
|
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
const response = await page.goto(server.EMPTY_PAGE, {
|
const response = await page.goto(server.EMPTY_PAGE, {
|
||||||
waitUntil: 'networkidle2',
|
waitUntil: 'networkidle2',
|
||||||
});
|
});
|
||||||
expect(response!.status()).toBe(200);
|
expect(response!.status()).toBe(200);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should fail when navigating to bad url', async () => {
|
it('should fail when navigating to bad url', async () => {
|
||||||
const {page, isChrome} = getTestState();
|
const {page, isChrome} = getTestState();
|
||||||
|
|
||||||
|
|
@ -332,85 +326,79 @@ describe('navigation', function () {
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
});
|
});
|
||||||
it(
|
it('should wait for network idle to succeed navigation', async () => {
|
||||||
'should wait for network idle to succeed navigation',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
let responses: ServerResponse[] = [];
|
let responses: ServerResponse[] = [];
|
||||||
// Hold on to a bunch of requests without answering.
|
// Hold on to a bunch of requests without answering.
|
||||||
server.setRoute('/fetch-request-a.js', (_req, res) => {
|
server.setRoute('/fetch-request-a.js', (_req, res) => {
|
||||||
return responses.push(res);
|
return responses.push(res);
|
||||||
});
|
});
|
||||||
server.setRoute('/fetch-request-b.js', (_req, res) => {
|
server.setRoute('/fetch-request-b.js', (_req, res) => {
|
||||||
return responses.push(res);
|
return responses.push(res);
|
||||||
});
|
});
|
||||||
server.setRoute('/fetch-request-c.js', (_req, res) => {
|
server.setRoute('/fetch-request-c.js', (_req, res) => {
|
||||||
return responses.push(res);
|
return responses.push(res);
|
||||||
});
|
});
|
||||||
server.setRoute('/fetch-request-d.js', (_req, res) => {
|
server.setRoute('/fetch-request-d.js', (_req, res) => {
|
||||||
return responses.push(res);
|
return responses.push(res);
|
||||||
});
|
});
|
||||||
const initialFetchResourcesRequested = Promise.all([
|
const initialFetchResourcesRequested = Promise.all([
|
||||||
server.waitForRequest('/fetch-request-a.js'),
|
server.waitForRequest('/fetch-request-a.js'),
|
||||||
server.waitForRequest('/fetch-request-b.js'),
|
server.waitForRequest('/fetch-request-b.js'),
|
||||||
server.waitForRequest('/fetch-request-c.js'),
|
server.waitForRequest('/fetch-request-c.js'),
|
||||||
]);
|
]);
|
||||||
const secondFetchResourceRequested = server.waitForRequest(
|
const secondFetchResourceRequested = server.waitForRequest(
|
||||||
'/fetch-request-d.js'
|
'/fetch-request-d.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate to a page which loads immediately and then does a bunch of
|
// Navigate to a page which loads immediately and then does a bunch of
|
||||||
// requests via javascript's fetch method.
|
// requests via javascript's fetch method.
|
||||||
const navigationPromise = page.goto(
|
const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', {
|
||||||
server.PREFIX + '/networkidle.html',
|
waitUntil: 'networkidle0',
|
||||||
{
|
});
|
||||||
waitUntil: 'networkidle0',
|
// Track when the navigation gets completed.
|
||||||
}
|
let navigationFinished = false;
|
||||||
);
|
navigationPromise.then(() => {
|
||||||
// Track when the navigation gets completed.
|
return (navigationFinished = true);
|
||||||
let navigationFinished = false;
|
});
|
||||||
navigationPromise.then(() => {
|
|
||||||
return (navigationFinished = true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for the page's 'load' event.
|
// Wait for the page's 'load' event.
|
||||||
await new Promise(fulfill => {
|
await new Promise(fulfill => {
|
||||||
return page.once('load', fulfill);
|
return page.once('load', fulfill);
|
||||||
});
|
});
|
||||||
expect(navigationFinished).toBe(false);
|
expect(navigationFinished).toBe(false);
|
||||||
|
|
||||||
// Wait for the initial three resources to be requested.
|
// Wait for the initial three resources to be requested.
|
||||||
await initialFetchResourcesRequested;
|
await initialFetchResourcesRequested;
|
||||||
|
|
||||||
// Expect navigation still to be not finished.
|
// Expect navigation still to be not finished.
|
||||||
expect(navigationFinished).toBe(false);
|
expect(navigationFinished).toBe(false);
|
||||||
|
|
||||||
// Respond to initial requests.
|
// Respond to initial requests.
|
||||||
for (const response of responses) {
|
for (const response of responses) {
|
||||||
response.statusCode = 404;
|
response.statusCode = 404;
|
||||||
response.end(`File not found`);
|
response.end(`File not found`);
|
||||||
}
|
|
||||||
|
|
||||||
// Reset responses array
|
|
||||||
responses = [];
|
|
||||||
|
|
||||||
// Wait for the second round to be requested.
|
|
||||||
await secondFetchResourceRequested;
|
|
||||||
// Expect navigation still to be not finished.
|
|
||||||
expect(navigationFinished).toBe(false);
|
|
||||||
|
|
||||||
// Respond to requests.
|
|
||||||
for (const response of responses) {
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.end(`File not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = (await navigationPromise)!;
|
|
||||||
// Expect navigation to succeed.
|
|
||||||
expect(response.ok()).toBe(true);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Reset responses array
|
||||||
|
responses = [];
|
||||||
|
|
||||||
|
// Wait for the second round to be requested.
|
||||||
|
await secondFetchResourceRequested;
|
||||||
|
// Expect navigation still to be not finished.
|
||||||
|
expect(navigationFinished).toBe(false);
|
||||||
|
|
||||||
|
// Respond to requests.
|
||||||
|
for (const response of responses) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.end(`File not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = (await navigationPromise)!;
|
||||||
|
// Expect navigation to succeed.
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
});
|
||||||
it('should not leak listeners during navigation', async () => {
|
it('should not leak listeners during navigation', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -459,38 +447,32 @@ describe('navigation', function () {
|
||||||
process.removeListener('warning', warningHandler);
|
process.removeListener('warning', warningHandler);
|
||||||
expect(warning).toBe(null);
|
expect(warning).toBe(null);
|
||||||
});
|
});
|
||||||
it(
|
it('should navigate to dataURL and fire dataURL requests', async () => {
|
||||||
'should navigate to dataURL and fire dataURL requests',
|
const {page} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
const requests: HTTPRequest[] = [];
|
const requests: HTTPRequest[] = [];
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
return !utils.isFavicon(request) && requests.push(request);
|
return !utils.isFavicon(request) && requests.push(request);
|
||||||
});
|
});
|
||||||
const dataURL = 'data:text/html,<div>yo</div>';
|
const dataURL = 'data:text/html,<div>yo</div>';
|
||||||
const response = (await page.goto(dataURL))!;
|
const response = (await page.goto(dataURL))!;
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
expect(requests.length).toBe(1);
|
expect(requests.length).toBe(1);
|
||||||
expect(requests[0]!.url()).toBe(dataURL);
|
expect(requests[0]!.url()).toBe(dataURL);
|
||||||
}
|
});
|
||||||
);
|
it('should navigate to URL with hash and fire requests without hash', async () => {
|
||||||
it(
|
const {page, server} = getTestState();
|
||||||
'should navigate to URL with hash and fire requests without hash',
|
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
const requests: HTTPRequest[] = [];
|
const requests: HTTPRequest[] = [];
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
return !utils.isFavicon(request) && requests.push(request);
|
return !utils.isFavicon(request) && requests.push(request);
|
||||||
});
|
});
|
||||||
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
|
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
expect(requests.length).toBe(1);
|
expect(requests.length).toBe(1);
|
||||||
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
|
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should work with self requesting page', async () => {
|
it('should work with self requesting page', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -614,13 +596,11 @@ describe('navigation', function () {
|
||||||
expect(response).toBe(null);
|
expect(response).toBe(null);
|
||||||
expect(page.url()).toBe(server.PREFIX + '/replaced.html');
|
expect(page.url()).toBe(server.PREFIX + '/replaced.html');
|
||||||
});
|
});
|
||||||
it(
|
it('should work with DOM history.back()/history.forward()', async () => {
|
||||||
'should work with DOM history.back()/history.forward()',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<a id=back onclick='javascript:goBack()'>back</a>
|
<a id=back onclick='javascript:goBack()'>back</a>
|
||||||
<a id=forward onclick='javascript:goForward()'>forward</a>
|
<a id=forward onclick='javascript:goForward()'>forward</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -630,46 +610,42 @@ describe('navigation', function () {
|
||||||
history.pushState({}, '', '/second.html');
|
history.pushState({}, '', '/second.html');
|
||||||
</script>
|
</script>
|
||||||
`);
|
`);
|
||||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||||
const [backResponse] = await Promise.all([
|
const [backResponse] = await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.click('a#back'),
|
page.click('a#back'),
|
||||||
]);
|
]);
|
||||||
expect(backResponse).toBe(null);
|
expect(backResponse).toBe(null);
|
||||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||||
const [forwardResponse] = await Promise.all([
|
const [forwardResponse] = await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.click('a#forward'),
|
page.click('a#forward'),
|
||||||
]);
|
]);
|
||||||
expect(forwardResponse).toBe(null);
|
expect(forwardResponse).toBe(null);
|
||||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||||
}
|
});
|
||||||
);
|
it('should work when subframe issues window.stop()', async () => {
|
||||||
it(
|
const {page, server} = getTestState();
|
||||||
'should work when subframe issues window.stop()',
|
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
server.setRoute('/frames/style.css', () => {});
|
server.setRoute('/frames/style.css', () => {});
|
||||||
const navigationPromise = page.goto(
|
const navigationPromise = page.goto(
|
||||||
server.PREFIX + '/frames/one-frame.html'
|
server.PREFIX + '/frames/one-frame.html'
|
||||||
);
|
);
|
||||||
const frame = await utils.waitEvent(page, 'frameattached');
|
const frame = await utils.waitEvent(page, 'frameattached');
|
||||||
await new Promise<void>(fulfill => {
|
await new Promise<void>(fulfill => {
|
||||||
page.on('framenavigated', f => {
|
page.on('framenavigated', f => {
|
||||||
if (f === frame) {
|
if (f === frame) {
|
||||||
fulfill();
|
fulfill();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
await Promise.all([
|
});
|
||||||
frame.evaluate(() => {
|
await Promise.all([
|
||||||
return window.stop();
|
frame.evaluate(() => {
|
||||||
}),
|
return window.stop();
|
||||||
navigationPromise,
|
}),
|
||||||
]);
|
navigationPromise,
|
||||||
}
|
]);
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Page.goBack', function () {
|
describe('Page.goBack', function () {
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,6 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
itFailsFirefox,
|
|
||||||
itChromeOnly,
|
|
||||||
itFirefoxOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
|
import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
|
||||||
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
||||||
|
|
@ -114,13 +111,13 @@ describe('network', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Request.headers', function () {
|
describe('Request.headers', function () {
|
||||||
itChromeOnly('should define Chrome as user agent header', async () => {
|
it('should define Chrome as user agent header', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
expect(response.request().headers()['user-agent']).toContain('Chrome');
|
expect(response.request().headers()['user-agent']).toContain('Chrome');
|
||||||
});
|
});
|
||||||
|
|
||||||
itFirefoxOnly('should define Firefox as user agent header', async () => {
|
it('should define Firefox as user agent header', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
const response = (await page.goto(server.EMPTY_PAGE))!;
|
const response = (await page.goto(server.EMPTY_PAGE))!;
|
||||||
|
|
@ -655,10 +652,7 @@ describe('network', function () {
|
||||||
expect(requests.get('script.js').isNavigationRequest()).toBe(false);
|
expect(requests.get('script.js').isNavigationRequest()).toBe(false);
|
||||||
expect(requests.get('style.css').isNavigationRequest()).toBe(false);
|
expect(requests.get('style.css').isNavigationRequest()).toBe(false);
|
||||||
});
|
});
|
||||||
// This `itFailsFirefox` should be preserved in mozilla-central (Firefox).
|
it('should work when navigating to image', async () => {
|
||||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748254
|
|
||||||
// or https://github.com/puppeteer/puppeteer/pull/7846
|
|
||||||
itFailsFirefox('should work when navigating to image', async () => {
|
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
const requests: HTTPRequest[] = [];
|
const requests: HTTPRequest[] = [];
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,11 @@
|
||||||
|
|
||||||
import utils from './utils.js';
|
import utils from './utils.js';
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {
|
import {getTestState} from './mocha-utils.js';
|
||||||
getTestState,
|
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||||
describeChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
import {
|
|
||||||
Browser,
|
|
||||||
BrowserContext,
|
|
||||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
|
||||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
|
|
||||||
describeChromeOnly('OOPIF', function () {
|
describe('OOPIF', function () {
|
||||||
/* We use a special browser for this test as we need the --site-per-process flag */
|
/* We use a special browser for this test as we need the --site-per-process flag */
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
let context: BrowserContext;
|
let context: BrowserContext;
|
||||||
|
|
@ -206,6 +200,7 @@ describeChromeOnly('OOPIF', function () {
|
||||||
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
|
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
expect(frame.url()).toBe(server.EMPTY_PAGE);
|
expect(frame.url()).toBe(server.EMPTY_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support evaluating in oop iframes', async () => {
|
it('should support evaluating in oop iframes', async () => {
|
||||||
const {server} = getTestState();
|
const {server} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
|
||||||
import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
import {
|
import {
|
||||||
getTestState,
|
getTestState,
|
||||||
itFailsFirefox,
|
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
@ -546,39 +545,69 @@ describe('Page', function () {
|
||||||
it('should work', async () => {
|
it('should work', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
// Instantiate an object
|
// Create a custom class
|
||||||
await page.evaluate(() => {
|
const classHandle = await page.evaluateHandle(() => {
|
||||||
return ((globalThis as any).set = new Set(['hello', 'world']));
|
return class CustomClass {};
|
||||||
});
|
});
|
||||||
const prototypeHandle = await page.evaluateHandle(() => {
|
|
||||||
return Set.prototype;
|
|
||||||
});
|
|
||||||
const objectsHandle = await page.queryObjects(prototypeHandle);
|
|
||||||
const count = await page.evaluate(objects => {
|
|
||||||
return objects.length;
|
|
||||||
}, objectsHandle);
|
|
||||||
expect(count).toBe(1);
|
|
||||||
const values = await page.evaluate(objects => {
|
|
||||||
return Array.from(objects[0]!.values());
|
|
||||||
}, objectsHandle);
|
|
||||||
expect(values).toEqual(['hello', 'world']);
|
|
||||||
});
|
|
||||||
it('should work for non-blank page', async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
// Instantiate an object
|
// Create an instance.
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.evaluate(CustomClass => {
|
||||||
await page.evaluate(() => {
|
// @ts-expect-error: Different context.
|
||||||
return ((globalThis as any).set = new Set(['hello', 'world']));
|
self.customClass = new CustomClass();
|
||||||
});
|
}, classHandle);
|
||||||
const prototypeHandle = await page.evaluateHandle(() => {
|
|
||||||
return Set.prototype;
|
// Validate only one has been added.
|
||||||
});
|
const prototypeHandle = await page.evaluateHandle(CustomClass => {
|
||||||
|
return CustomClass.prototype;
|
||||||
|
}, classHandle);
|
||||||
const objectsHandle = await page.queryObjects(prototypeHandle);
|
const objectsHandle = await page.queryObjects(prototypeHandle);
|
||||||
const count = await page.evaluate(objects => {
|
await expect(
|
||||||
return objects.length;
|
page.evaluate(objects => {
|
||||||
}, objectsHandle);
|
return objects.length;
|
||||||
expect(count).toBe(1);
|
}, objectsHandle)
|
||||||
|
).resolves.toBe(1);
|
||||||
|
|
||||||
|
// Check that instances.
|
||||||
|
await expect(
|
||||||
|
page.evaluate(objects => {
|
||||||
|
// @ts-expect-error: Different context.
|
||||||
|
return objects[0] === self.customClass;
|
||||||
|
}, objectsHandle)
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should work for non-trivial page', async () => {
|
||||||
|
const {page, server} = getTestState();
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
||||||
|
// Create a custom class
|
||||||
|
const classHandle = await page.evaluateHandle(() => {
|
||||||
|
return class CustomClass {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an instance.
|
||||||
|
await page.evaluate(CustomClass => {
|
||||||
|
// @ts-expect-error: Different context.
|
||||||
|
self.customClass = new CustomClass();
|
||||||
|
}, classHandle);
|
||||||
|
|
||||||
|
// Validate only one has been added.
|
||||||
|
const prototypeHandle = await page.evaluateHandle(CustomClass => {
|
||||||
|
return CustomClass.prototype;
|
||||||
|
}, classHandle);
|
||||||
|
const objectsHandle = await page.queryObjects(prototypeHandle);
|
||||||
|
await expect(
|
||||||
|
page.evaluate(objects => {
|
||||||
|
return objects.length;
|
||||||
|
}, objectsHandle)
|
||||||
|
).resolves.toBe(1);
|
||||||
|
|
||||||
|
// Check that instances.
|
||||||
|
await expect(
|
||||||
|
page.evaluate(objects => {
|
||||||
|
// @ts-expect-error: Different context.
|
||||||
|
return objects[0] === self.customClass;
|
||||||
|
}, objectsHandle)
|
||||||
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should fail for disposed handles', async () => {
|
it('should fail for disposed handles', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
@ -1651,7 +1680,7 @@ describe('Page', function () {
|
||||||
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
|
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
@ -1664,10 +1693,12 @@ describe('Page', function () {
|
||||||
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
||||||
type: 'module',
|
type: 'module',
|
||||||
});
|
});
|
||||||
await page.waitForFunction('window.__es6injected');
|
await page.waitForFunction(() => {
|
||||||
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
@ -1680,10 +1711,12 @@ describe('Page', function () {
|
||||||
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
||||||
type: 'module',
|
type: 'module',
|
||||||
});
|
});
|
||||||
await page.waitForFunction('window.__es6injected');
|
await page.waitForFunction(() => {
|
||||||
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
@ -1758,7 +1791,7 @@ describe('Page', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @see https://github.com/puppeteer/puppeteer/issues/4840
|
// @see https://github.com/puppeteer/puppeteer/issues/4840
|
||||||
xit('should throw when added with content to the CSP page', async () => {
|
it.skip('should throw when added with content to the CSP page', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/csp.html');
|
await page.goto(server.PREFIX + '/csp.html');
|
||||||
|
|
@ -1854,7 +1887,7 @@ describe('Page', function () {
|
||||||
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
||||||
});
|
});
|
||||||
const styleHandle = (await page.$('style'))!;
|
const styleHandle = (await page.$('style'))!;
|
||||||
const styleContent = await page.evaluate(style => {
|
const styleContent = await page.evaluate((style: HTMLStyleElement) => {
|
||||||
return style.innerHTML;
|
return style.innerHTML;
|
||||||
}, styleHandle);
|
}, styleHandle);
|
||||||
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
||||||
|
|
@ -2002,10 +2035,7 @@ describe('Page', function () {
|
||||||
expect(size).toBeGreaterThan(0);
|
expect(size).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This test should be skipped in mozilla-central (Firefox).
|
it('should respect timeout', async () => {
|
||||||
// It intermittently makes the whole test suite fail.
|
|
||||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255
|
|
||||||
itFailsFirefox('should respect timeout', async () => {
|
|
||||||
const {isHeadless, page, server, puppeteer} = getTestState();
|
const {isHeadless, page, server, puppeteer} = getTestState();
|
||||||
if (!isHeadless) {
|
if (!isHeadless) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2236,7 +2266,7 @@ describe('Page', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Page.Events.Close', function () {
|
describe('Page.Events.Close', function () {
|
||||||
itFailsFirefox('should work with window.close', async () => {
|
it('should work with window.close', async () => {
|
||||||
const {page, context} = getTestState();
|
const {page, context} = getTestState();
|
||||||
|
|
||||||
const newPagePromise = new Promise<Page>(fulfill => {
|
const newPagePromise = new Promise<Page>(fulfill => {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,9 @@
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import {
|
import {getTestState} from './mocha-utils.js';
|
||||||
getTestState,
|
|
||||||
describeFailsFirefox,
|
|
||||||
itFailsWindows,
|
|
||||||
} from './mocha-utils.js';
|
|
||||||
import type {Server, IncomingMessage, ServerResponse} from 'http';
|
import type {Server, IncomingMessage, ServerResponse} from 'http';
|
||||||
import type {Browser} from '../../lib/cjs/puppeteer/common/Browser.js';
|
import type {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||||
import type {AddressInfo} from 'net';
|
import type {AddressInfo} from 'net';
|
||||||
import {TestServer} from '../../utils/testserver/lib/index.js';
|
import {TestServer} from '../../utils/testserver/lib/index.js';
|
||||||
|
|
||||||
|
|
@ -53,7 +49,7 @@ function getEmptyPageUrl(server: TestServer): string {
|
||||||
return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
|
return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
describeFailsFirefox('request proxy', () => {
|
describe('request proxy', () => {
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
let proxiedRequestUrls: string[];
|
let proxiedRequestUrls: string[];
|
||||||
let proxyServer: Server;
|
let proxyServer: Server;
|
||||||
|
|
@ -194,28 +190,25 @@ describeFailsFirefox('request proxy', () => {
|
||||||
/**
|
/**
|
||||||
* See issues #7873, #7719, and #7698.
|
* See issues #7873, #7719, and #7698.
|
||||||
*/
|
*/
|
||||||
itFailsWindows(
|
it('should proxy requests when configured at context level', async () => {
|
||||||
'should proxy requests when configured at context level',
|
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
||||||
async () => {
|
const emptyPageUrl = getEmptyPageUrl(server);
|
||||||
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
|
||||||
const emptyPageUrl = getEmptyPageUrl(server);
|
|
||||||
|
|
||||||
browser = await puppeteer.launch({
|
browser = await puppeteer.launch({
|
||||||
...defaultBrowserOptions,
|
...defaultBrowserOptions,
|
||||||
args: defaultArgs,
|
args: defaultArgs,
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = await browser.createIncognitoBrowserContext({
|
const context = await browser.createIncognitoBrowserContext({
|
||||||
proxyServer: proxyServerUrl,
|
proxyServer: proxyServerUrl,
|
||||||
});
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const response = (await page.goto(emptyPageUrl))!;
|
const response = (await page.goto(emptyPageUrl))!;
|
||||||
|
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
|
|
||||||
expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
|
expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('should respect proxy bypass list when configured at context level', async () => {
|
it('should respect proxy bypass list when configured at context level', async () => {
|
||||||
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,200 @@ describe('Query handler tests', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Text selectors', function () {
|
||||||
|
describe('in Page', function () {
|
||||||
|
it('should query existing element', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<section>test</section>');
|
||||||
|
|
||||||
|
expect(await page.$('text/test')).toBeTruthy();
|
||||||
|
expect((await page.$$('text/test')).length).toBe(1);
|
||||||
|
});
|
||||||
|
it('should return empty array for non-existing element', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
expect(await page.$('text/test')).toBeFalsy();
|
||||||
|
expect((await page.$$('text/test')).length).toBe(0);
|
||||||
|
});
|
||||||
|
it('should return first element', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div id="1">a</div><div>a</div>');
|
||||||
|
|
||||||
|
const element = await page.$('text/a');
|
||||||
|
expect(
|
||||||
|
await element?.evaluate(e => {
|
||||||
|
return e.id;
|
||||||
|
})
|
||||||
|
).toBe('1');
|
||||||
|
});
|
||||||
|
it('should return multiple elements', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div>a</div><div>a</div>');
|
||||||
|
|
||||||
|
const elements = await page.$$('text/a');
|
||||||
|
expect(elements.length).toBe(2);
|
||||||
|
});
|
||||||
|
it('should pierce shadow DOM', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const shadow = div.attachShadow({mode: 'open'});
|
||||||
|
const diva = document.createElement('div');
|
||||||
|
shadow.append(diva);
|
||||||
|
const divb = document.createElement('div');
|
||||||
|
shadow.append(divb);
|
||||||
|
diva.innerHTML = 'a';
|
||||||
|
divb.innerHTML = 'b';
|
||||||
|
document.body.append(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = await page.$('text/a');
|
||||||
|
expect(
|
||||||
|
await element?.evaluate(e => {
|
||||||
|
return e.textContent;
|
||||||
|
})
|
||||||
|
).toBe('a');
|
||||||
|
});
|
||||||
|
it('should query deeply nested text', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div><div>a</div><div>b</div></div>');
|
||||||
|
|
||||||
|
const element = await page.$('text/a');
|
||||||
|
expect(
|
||||||
|
await element?.evaluate(e => {
|
||||||
|
return e.textContent;
|
||||||
|
})
|
||||||
|
).toBe('a');
|
||||||
|
});
|
||||||
|
it('should query inputs', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<input value="a">');
|
||||||
|
|
||||||
|
const element = (await page.$(
|
||||||
|
'text/a'
|
||||||
|
)) as ElementHandle<HTMLInputElement>;
|
||||||
|
expect(
|
||||||
|
await element?.evaluate(e => {
|
||||||
|
return e.value;
|
||||||
|
})
|
||||||
|
).toBe('a');
|
||||||
|
});
|
||||||
|
it('should not query radio', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<radio value="a">');
|
||||||
|
|
||||||
|
expect(await page.$('text/a')).toBeNull();
|
||||||
|
});
|
||||||
|
it('should query text spanning multiple elements', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div><span>a</span> <span>b</span><div>');
|
||||||
|
|
||||||
|
const element = await page.$('text/a b');
|
||||||
|
expect(
|
||||||
|
await element?.evaluate(e => {
|
||||||
|
return e.textContent;
|
||||||
|
})
|
||||||
|
).toBe('a b');
|
||||||
|
});
|
||||||
|
it('should clear caches', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent(
|
||||||
|
'<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>'
|
||||||
|
);
|
||||||
|
const div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>;
|
||||||
|
const input = (await page.$(
|
||||||
|
'#target2'
|
||||||
|
)) as ElementHandle<HTMLInputElement>;
|
||||||
|
|
||||||
|
await div.evaluate(div => {
|
||||||
|
div.textContent = 'text';
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await page.$eval(`text/text`, e => {
|
||||||
|
return e.id;
|
||||||
|
})
|
||||||
|
).toBe('target1');
|
||||||
|
await div.evaluate(div => {
|
||||||
|
div.textContent = 'foo';
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await page.$eval(`text/text`, e => {
|
||||||
|
return e.id;
|
||||||
|
})
|
||||||
|
).toBe('target2');
|
||||||
|
await input.evaluate(input => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
await input.type('foo');
|
||||||
|
expect(
|
||||||
|
await page.$eval(`text/text`, e => {
|
||||||
|
return e.id;
|
||||||
|
})
|
||||||
|
).toBe('target3');
|
||||||
|
|
||||||
|
await div.evaluate(div => {
|
||||||
|
div.textContent = 'text';
|
||||||
|
});
|
||||||
|
await input.evaluate(input => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
await input.type('text');
|
||||||
|
expect(
|
||||||
|
await page.$$eval(`text/text`, es => {
|
||||||
|
return es.length;
|
||||||
|
})
|
||||||
|
).toBe(3);
|
||||||
|
await div.evaluate(div => {
|
||||||
|
div.textContent = 'foo';
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await page.$$eval(`text/text`, es => {
|
||||||
|
return es.length;
|
||||||
|
})
|
||||||
|
).toBe(2);
|
||||||
|
await input.evaluate(input => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
await input.type('foo');
|
||||||
|
expect(
|
||||||
|
await page.$$eval(`text/text`, es => {
|
||||||
|
return es.length;
|
||||||
|
})
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('in ElementHandles', function () {
|
||||||
|
it('should query existing element', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div class="a"><span>a</span></div>');
|
||||||
|
|
||||||
|
const elementHandle = (await page.$('div'))!;
|
||||||
|
expect(await elementHandle.$(`text/a`)).toBeTruthy();
|
||||||
|
expect((await elementHandle.$$(`text/a`)).length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existing element', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
await page.setContent('<div class="a"></div>');
|
||||||
|
|
||||||
|
const elementHandle = (await page.$('div'))!;
|
||||||
|
expect(await elementHandle.$(`text/a`)).toBeFalsy();
|
||||||
|
expect((await elementHandle.$$(`text/a`)).length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('XPath selectors', function () {
|
describe('XPath selectors', function () {
|
||||||
describe('in Page', function () {
|
describe('in Page', function () {
|
||||||
it('should query existing element', async () => {
|
it('should query existing element', async () => {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ import {
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
itHeadfulOnly,
|
|
||||||
itChromeOnly,
|
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
describe('Screenshots', function () {
|
describe('Screenshots', function () {
|
||||||
|
|
@ -67,23 +65,20 @@ describe('Screenshots', function () {
|
||||||
});
|
});
|
||||||
expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
|
expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
|
||||||
});
|
});
|
||||||
it(
|
it('should get screenshot bigger than the viewport', async () => {
|
||||||
'should get screenshot bigger than the viewport',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
await page.setViewport({width: 50, height: 50});
|
||||||
const {page, server} = getTestState();
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
await page.setViewport({width: 50, height: 50});
|
const screenshot = await page.screenshot({
|
||||||
await page.goto(server.PREFIX + '/grid.html');
|
clip: {
|
||||||
const screenshot = await page.screenshot({
|
x: 25,
|
||||||
clip: {
|
y: 25,
|
||||||
x: 25,
|
width: 100,
|
||||||
y: 25,
|
height: 100,
|
||||||
width: 100,
|
},
|
||||||
height: 100,
|
});
|
||||||
},
|
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
||||||
});
|
});
|
||||||
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
it('should run in parallel', async () => {
|
it('should run in parallel', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
|
|
@ -205,7 +200,7 @@ describe('Screenshots', function () {
|
||||||
'screenshot-sanity.png'
|
'screenshot-sanity.png'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
itHeadfulOnly('should work in "fromSurface: false" mode', async () => {
|
it('should work in "fromSurface: false" mode', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
@ -230,7 +225,7 @@ describe('Screenshots', function () {
|
||||||
const screenshot = await elementHandle.screenshot();
|
const screenshot = await elementHandle.screenshot();
|
||||||
expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
|
expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
|
||||||
});
|
});
|
||||||
itChromeOnly('should work with a null viewport', async () => {
|
it('should work with a null viewport', async () => {
|
||||||
const {defaultBrowserOptions, puppeteer, server} = getTestState();
|
const {defaultBrowserOptions, puppeteer, server} = getTestState();
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
|
|
@ -270,14 +265,12 @@ describe('Screenshots', function () {
|
||||||
const screenshot = await elementHandle.screenshot();
|
const screenshot = await elementHandle.screenshot();
|
||||||
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
|
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
|
||||||
});
|
});
|
||||||
it(
|
it('should capture full element when larger than viewport', async () => {
|
||||||
'should capture full element when larger than viewport',
|
const {page} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
await page.setViewport({width: 500, height: 500});
|
await page.setViewport({width: 500, height: 500});
|
||||||
|
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
something above
|
something above
|
||||||
<style>
|
<style>
|
||||||
div.to-screenshot {
|
div.to-screenshot {
|
||||||
|
|
@ -292,22 +285,21 @@ describe('Screenshots', function () {
|
||||||
</style>
|
</style>
|
||||||
<div class="to-screenshot"></div>
|
<div class="to-screenshot"></div>
|
||||||
`);
|
`);
|
||||||
const elementHandle = (await page.$('div.to-screenshot'))!;
|
const elementHandle = (await page.$('div.to-screenshot'))!;
|
||||||
const screenshot = await elementHandle.screenshot();
|
const screenshot = await elementHandle.screenshot();
|
||||||
expect(screenshot).toBeGolden(
|
expect(screenshot).toBeGolden(
|
||||||
'screenshot-element-larger-than-viewport.png'
|
'screenshot-element-larger-than-viewport.png'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return {
|
return {
|
||||||
w: window.innerWidth,
|
w: window.innerWidth,
|
||||||
h: window.innerHeight,
|
h: window.innerHeight,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
).toEqual({w: 500, h: 500});
|
).toEqual({w: 500, h: 500});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should scroll element into view', async () => {
|
it('should scroll element into view', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
import {Target} from '../../lib/cjs/puppeteer/common/Target.js';
|
import {Target} from '../../lib/cjs/puppeteer/common/Target.js';
|
||||||
import {
|
import {
|
||||||
getTestState,
|
getTestState,
|
||||||
itFailsFirefox,
|
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
import utils from './utils.js';
|
import utils from './utils.js';
|
||||||
|
|
||||||
const {waitEvent} = utils;
|
const {waitEvent} = utils;
|
||||||
|
|
||||||
describe('Target', function () {
|
describe('Target', function () {
|
||||||
|
|
@ -79,10 +79,7 @@ describe('Target', function () {
|
||||||
).toBe('Hello world');
|
).toBe('Hello world');
|
||||||
expect(await originalPage.$('body')).toBeTruthy();
|
expect(await originalPage.$('body')).toBeTruthy();
|
||||||
});
|
});
|
||||||
// This test should be skipped in mozilla-central (Firefox).
|
it('should be able to use async waitForTarget', async () => {
|
||||||
// It intermittently makes some tests fail and triggers errors in the test hooks.
|
|
||||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255
|
|
||||||
itFailsFirefox('should be able to use async waitForTarget', async () => {
|
|
||||||
const {page, server, context} = getTestState();
|
const {page, server, context} = getTestState();
|
||||||
|
|
||||||
const [otherPage] = await Promise.all([
|
const [otherPage] = await Promise.all([
|
||||||
|
|
@ -104,88 +101,82 @@ describe('Target', function () {
|
||||||
);
|
);
|
||||||
expect(page).not.toEqual(otherPage);
|
expect(page).not.toEqual(otherPage);
|
||||||
});
|
});
|
||||||
it(
|
it('should report when a new page is created and closed', async () => {
|
||||||
'should report when a new page is created and closed',
|
const {page, server, context} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server, context} = getTestState();
|
|
||||||
|
|
||||||
const [otherPage] = await Promise.all([
|
const [otherPage] = await Promise.all([
|
||||||
context
|
context
|
||||||
.waitForTarget(target => {
|
.waitForTarget(target => {
|
||||||
return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
|
return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
|
||||||
})
|
|
||||||
.then(target => {
|
|
||||||
return target.page();
|
|
||||||
}),
|
|
||||||
page.evaluate((url: string) => {
|
|
||||||
return window.open(url);
|
|
||||||
}, server.CROSS_PROCESS_PREFIX + '/empty.html'),
|
|
||||||
]);
|
|
||||||
expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
|
|
||||||
expect(
|
|
||||||
await otherPage!.evaluate(() => {
|
|
||||||
return ['Hello', 'world'].join(' ');
|
|
||||||
})
|
})
|
||||||
).toBe('Hello world');
|
.then(target => {
|
||||||
expect(await otherPage!.$('body')).toBeTruthy();
|
|
||||||
|
|
||||||
let allPages = await context.pages();
|
|
||||||
expect(allPages).toContain(page);
|
|
||||||
expect(allPages).toContain(otherPage);
|
|
||||||
|
|
||||||
const closePagePromise = new Promise(fulfill => {
|
|
||||||
return context.once('targetdestroyed', target => {
|
|
||||||
return fulfill(target.page());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await otherPage!.close();
|
|
||||||
expect(await closePagePromise).toBe(otherPage);
|
|
||||||
|
|
||||||
allPages = (await Promise.all(
|
|
||||||
context.targets().map(target => {
|
|
||||||
return target.page();
|
return target.page();
|
||||||
})
|
}),
|
||||||
)) as Page[];
|
page.evaluate((url: string) => {
|
||||||
expect(allPages).toContain(page);
|
return window.open(url);
|
||||||
expect(allPages).not.toContain(otherPage);
|
}, server.CROSS_PROCESS_PREFIX + '/empty.html'),
|
||||||
}
|
]);
|
||||||
);
|
expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
|
||||||
it(
|
expect(
|
||||||
'should report when a service worker is created and destroyed',
|
await otherPage!.evaluate(() => {
|
||||||
async () => {
|
return ['Hello', 'world'].join(' ');
|
||||||
const {page, server, context} = getTestState();
|
})
|
||||||
|
).toBe('Hello world');
|
||||||
|
expect(await otherPage!.$('body')).toBeTruthy();
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
let allPages = await context.pages();
|
||||||
const createdTarget = new Promise<Target>(fulfill => {
|
expect(allPages).toContain(page);
|
||||||
return context.once('targetcreated', target => {
|
expect(allPages).toContain(otherPage);
|
||||||
return fulfill(target);
|
|
||||||
});
|
const closePagePromise = new Promise(fulfill => {
|
||||||
|
return context.once('targetdestroyed', target => {
|
||||||
|
return fulfill(target.page());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
await otherPage!.close();
|
||||||
|
expect(await closePagePromise).toBe(otherPage);
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
|
allPages = (await Promise.all(
|
||||||
|
context.targets().map(target => {
|
||||||
|
return target.page();
|
||||||
|
})
|
||||||
|
)) as Page[];
|
||||||
|
expect(allPages).toContain(page);
|
||||||
|
expect(allPages).not.toContain(otherPage);
|
||||||
|
});
|
||||||
|
it('should report when a service worker is created and destroyed', async () => {
|
||||||
|
const {page, server, context} = getTestState();
|
||||||
|
|
||||||
expect((await createdTarget).type()).toBe('service_worker');
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect((await createdTarget).url()).toBe(
|
const createdTarget = new Promise<Target>(fulfill => {
|
||||||
server.PREFIX + '/serviceworkers/empty/sw.js'
|
return context.once('targetcreated', target => {
|
||||||
);
|
return fulfill(target);
|
||||||
|
|
||||||
const destroyedTarget = new Promise(fulfill => {
|
|
||||||
return context.once('targetdestroyed', target => {
|
|
||||||
return fulfill(target);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
await page.evaluate(() => {
|
});
|
||||||
return (
|
|
||||||
globalThis as unknown as {
|
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
|
||||||
registrationPromise: Promise<{unregister: () => void}>;
|
|
||||||
}
|
expect((await createdTarget).type()).toBe('service_worker');
|
||||||
).registrationPromise.then((registration: any) => {
|
expect((await createdTarget).url()).toBe(
|
||||||
return registration.unregister();
|
server.PREFIX + '/serviceworkers/empty/sw.js'
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const destroyedTarget = new Promise(fulfill => {
|
||||||
|
return context.once('targetdestroyed', target => {
|
||||||
|
return fulfill(target);
|
||||||
});
|
});
|
||||||
expect(await destroyedTarget).toBe(await createdTarget);
|
});
|
||||||
}
|
await page.evaluate(() => {
|
||||||
);
|
return (
|
||||||
|
globalThis as unknown as {
|
||||||
|
registrationPromise: Promise<{unregister: () => void}>;
|
||||||
|
}
|
||||||
|
).registrationPromise.then((registration: any) => {
|
||||||
|
return registration.unregister();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(await destroyedTarget).toBe(await createdTarget);
|
||||||
|
});
|
||||||
it('should create a worker from a service worker', async () => {
|
it('should create a worker from a service worker', async () => {
|
||||||
const {page, server, context} = getTestState();
|
const {page, server, context} = getTestState();
|
||||||
|
|
||||||
|
|
@ -271,36 +262,33 @@ describe('Target', function () {
|
||||||
expect(targetChanged).toBe(false);
|
expect(targetChanged).toBe(false);
|
||||||
context.removeListener('targetchanged', listener);
|
context.removeListener('targetchanged', listener);
|
||||||
});
|
});
|
||||||
it(
|
it('should not crash while redirecting if original request was missed', async () => {
|
||||||
'should not crash while redirecting if original request was missed',
|
const {page, server, context} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server, context} = getTestState();
|
|
||||||
|
|
||||||
let serverResponse!: ServerResponse;
|
let serverResponse!: ServerResponse;
|
||||||
server.setRoute('/one-style.css', (_req, res) => {
|
server.setRoute('/one-style.css', (_req, res) => {
|
||||||
return (serverResponse = res);
|
return (serverResponse = res);
|
||||||
});
|
});
|
||||||
// Open a new page. Use window.open to connect to the page later.
|
// Open a new page. Use window.open to connect to the page later.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.evaluate((url: string) => {
|
page.evaluate((url: string) => {
|
||||||
return window.open(url);
|
return window.open(url);
|
||||||
}, server.PREFIX + '/one-style.html'),
|
}, server.PREFIX + '/one-style.html'),
|
||||||
server.waitForRequest('/one-style.css'),
|
server.waitForRequest('/one-style.css'),
|
||||||
]);
|
]);
|
||||||
// Connect to the opened page.
|
// Connect to the opened page.
|
||||||
const target = await context.waitForTarget(target => {
|
const target = await context.waitForTarget(target => {
|
||||||
return target.url().includes('one-style.html');
|
return target.url().includes('one-style.html');
|
||||||
});
|
});
|
||||||
const newPage = (await target.page())!;
|
const newPage = (await target.page())!;
|
||||||
// Issue a redirect.
|
// Issue a redirect.
|
||||||
serverResponse.writeHead(302, {location: '/injectedstyle.css'});
|
serverResponse.writeHead(302, {location: '/injectedstyle.css'});
|
||||||
serverResponse.end();
|
serverResponse.end();
|
||||||
// Wait for the new page to load.
|
// Wait for the new page to load.
|
||||||
await waitEvent(newPage, 'load');
|
await waitEvent(newPage, 'load');
|
||||||
// Cleanup.
|
// Cleanup.
|
||||||
await newPage.close();
|
await newPage.close();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
it('should have an opener', async () => {
|
it('should have an opener', async () => {
|
||||||
const {page, server, context} = getTestState();
|
const {page, server, context} = getTestState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {getTestState, describeChromeOnly} from './mocha-utils.js';
|
import {getTestState} from './mocha-utils.js';
|
||||||
import {Browser} from '../../lib/cjs/puppeteer/common/Browser.js';
|
import {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||||
|
|
||||||
describeChromeOnly('Tracing', function () {
|
describe('Tracing', function () {
|
||||||
let outputFile!: string;
|
let outputFile!: string;
|
||||||
let browser!: Browser;
|
let browser!: Browser;
|
||||||
let page!: Page;
|
let page!: Page;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||||
import {
|
import {
|
||||||
|
createTimeout,
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
|
|
@ -31,9 +32,9 @@ describe('waittask specs', function () {
|
||||||
it('should accept a string', async () => {
|
it('should accept a string', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
const watchdog = page.waitForFunction('window.__FOO === 1');
|
const watchdog = page.waitForFunction('self.__FOO === 1');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return ((globalThis as any).__FOO = 1);
|
return ((self as unknown as {__FOO: number}).__FOO = 1);
|
||||||
});
|
});
|
||||||
await watchdog;
|
await watchdog;
|
||||||
});
|
});
|
||||||
|
|
@ -46,61 +47,25 @@ describe('waittask specs', function () {
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
if (!(globalThis as any).__RELOADED) {
|
if (!(globalThis as any).__RELOADED) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should poll on interval', async () => {
|
it('should poll on interval', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let success = false;
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const polling = 100;
|
const polling = 100;
|
||||||
const watchdog = page
|
const watchdog = page.waitForFunction(
|
||||||
.waitForFunction(
|
() => {
|
||||||
() => {
|
return (globalThis as any).__FOO === 'hit';
|
||||||
return (globalThis as any).__FOO === 'hit';
|
},
|
||||||
},
|
{polling}
|
||||||
{
|
);
|
||||||
polling,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
return (success = true);
|
|
||||||
});
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return ((globalThis as any).__FOO = 'hit');
|
setTimeout(() => {
|
||||||
});
|
(globalThis as any).__FOO = 'hit';
|
||||||
expect(success).toBe(false);
|
}, 50);
|
||||||
await page.evaluate(() => {
|
|
||||||
return document.body.appendChild(document.createElement('div'));
|
|
||||||
});
|
|
||||||
await watchdog;
|
|
||||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
|
||||||
});
|
|
||||||
it('should poll on interval async', async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
let success = false;
|
|
||||||
const startTime = Date.now();
|
|
||||||
const polling = 100;
|
|
||||||
const watchdog = page
|
|
||||||
.waitForFunction(
|
|
||||||
async () => {
|
|
||||||
return (globalThis as any).__FOO === 'hit';
|
|
||||||
},
|
|
||||||
{
|
|
||||||
polling,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
return (success = true);
|
|
||||||
});
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
return ((globalThis as any).__FOO = 'hit');
|
|
||||||
});
|
|
||||||
expect(success).toBe(false);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
return document.body.appendChild(document.createElement('div'));
|
|
||||||
});
|
});
|
||||||
await watchdog;
|
await watchdog;
|
||||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
||||||
|
|
@ -212,26 +177,6 @@ describe('waittask specs', function () {
|
||||||
]);
|
]);
|
||||||
expect(error).toBeUndefined();
|
expect(error).toBeUndefined();
|
||||||
});
|
});
|
||||||
it('should throw on bad polling value', async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
let error!: Error;
|
|
||||||
try {
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => {
|
|
||||||
return !!document.body;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
polling: 'unknown',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error_) {
|
|
||||||
if (isErrorLike(error_)) {
|
|
||||||
error = error_ as Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(error?.message).toContain('polling');
|
|
||||||
});
|
|
||||||
it('should throw negative polling interval', async () => {
|
it('should throw negative polling interval', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
|
@ -299,23 +244,34 @@ describe('waittask specs', function () {
|
||||||
const {page, puppeteer} = getTestState();
|
const {page, puppeteer} = getTestState();
|
||||||
|
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page.waitForFunction('false', {timeout: 10}).catch(error_ => {
|
await page
|
||||||
return (error = error_);
|
.waitForFunction(
|
||||||
});
|
() => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{timeout: 10}
|
||||||
|
)
|
||||||
|
.catch(error_ => {
|
||||||
|
return (error = error_);
|
||||||
|
});
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain('waiting for function failed: timeout');
|
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||||
});
|
});
|
||||||
it('should respect default timeout', async () => {
|
it('should respect default timeout', async () => {
|
||||||
const {page, puppeteer} = getTestState();
|
const {page, puppeteer} = getTestState();
|
||||||
|
|
||||||
page.setDefaultTimeout(1);
|
page.setDefaultTimeout(1);
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page.waitForFunction('false').catch(error_ => {
|
await page
|
||||||
return (error = error_);
|
.waitForFunction(() => {
|
||||||
});
|
return false;
|
||||||
|
})
|
||||||
|
.catch(error_ => {
|
||||||
|
return (error = error_);
|
||||||
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain('waiting for function failed: timeout');
|
expect(error?.message).toContain('Waiting failed: 1ms exceeded');
|
||||||
});
|
});
|
||||||
it('should disable timeout when its set to 0', async () => {
|
it('should disable timeout when its set to 0', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
@ -341,7 +297,9 @@ describe('waittask specs', function () {
|
||||||
|
|
||||||
let fooFound = false;
|
let fooFound = false;
|
||||||
const waitForFunction = page
|
const waitForFunction = page
|
||||||
.waitForFunction('globalThis.__FOO === 1')
|
.waitForFunction(() => {
|
||||||
|
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return (fooFound = true);
|
return (fooFound = true);
|
||||||
});
|
});
|
||||||
|
|
@ -464,21 +422,18 @@ describe('waittask specs', function () {
|
||||||
await watchdog;
|
await watchdog;
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it('Page.waitForSelector is shortcut for main frame', async () => {
|
||||||
'Page.waitForSelector is shortcut for main frame',
|
const {page, server} = getTestState();
|
||||||
async () => {
|
|
||||||
const {page, server} = getTestState();
|
|
||||||
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
const otherFrame = page.frames()[1]!;
|
const otherFrame = page.frames()[1]!;
|
||||||
const watchdog = page.waitForSelector('div');
|
const watchdog = page.waitForSelector('div');
|
||||||
await otherFrame.evaluate(addElement, 'div');
|
await otherFrame.evaluate(addElement, 'div');
|
||||||
await page.evaluate(addElement, 'div');
|
await page.evaluate(addElement, 'div');
|
||||||
const eHandle = await watchdog;
|
const eHandle = await watchdog;
|
||||||
expect(eHandle?.frame).toBe(page.mainFrame());
|
expect(eHandle?.frame).toBe(page.mainFrame());
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('should run in specified frame', async () => {
|
it('should run in specified frame', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
@ -525,113 +480,186 @@ describe('waittask specs', function () {
|
||||||
await waitForSelector;
|
await waitForSelector;
|
||||||
expect(boxFound).toBe(true);
|
expect(boxFound).toBe(true);
|
||||||
});
|
});
|
||||||
it('should wait for visible', async () => {
|
it('should wait for element to be visible (display)', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divFound = false;
|
const promise = page.waitForSelector('div', {visible: true});
|
||||||
const waitForSelector = page
|
await page.setContent('<div style="display: none">text</div>');
|
||||||
.waitForSelector('div', {visible: true})
|
const element = await page.evaluateHandle(() => {
|
||||||
.then(() => {
|
return document.getElementsByTagName('div')[0]!;
|
||||||
return (divFound = true);
|
|
||||||
});
|
|
||||||
await page.setContent(
|
|
||||||
`<div style='display: none; visibility: hidden;'>1</div>`
|
|
||||||
);
|
|
||||||
expect(divFound).toBe(false);
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return document.querySelector('div')?.style.removeProperty('display');
|
|
||||||
});
|
});
|
||||||
expect(divFound).toBe(false);
|
await expect(
|
||||||
await page.evaluate(() => {
|
Promise.race([promise, createTimeout(40)])
|
||||||
return document
|
).resolves.toBeFalsy();
|
||||||
.querySelector('div')
|
await element.evaluate(e => {
|
||||||
?.style.removeProperty('visibility');
|
e.style.removeProperty('display');
|
||||||
});
|
});
|
||||||
expect(await waitForSelector).toBe(true);
|
await expect(promise).resolves.toBeTruthy();
|
||||||
expect(divFound).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('should wait for visible recursively', async () => {
|
it('should wait for element to be visible (visibility)', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divVisible = false;
|
const promise = page.waitForSelector('div', {visible: true});
|
||||||
const waitForSelector = page
|
await page.setContent('<div style="visibility: hidden">text</div>');
|
||||||
.waitForSelector('div#inner', {visible: true})
|
const element = await page.evaluateHandle(() => {
|
||||||
.then(() => {
|
return document.getElementsByTagName('div')[0]!;
|
||||||
return (divVisible = true);
|
});
|
||||||
});
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('visibility', 'collapse');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.removeProperty('visibility');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should wait for element to be visible (bounding box)', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
const promise = page.waitForSelector('div', {visible: true});
|
||||||
|
await page.setContent('<div style="width: 0">text</div>');
|
||||||
|
const element = await page.evaluateHandle(() => {
|
||||||
|
return document.getElementsByTagName('div')[0]!;
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('height', '0');
|
||||||
|
e.style.removeProperty('width');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('position', 'absolute');
|
||||||
|
e.style.setProperty('right', '100vw');
|
||||||
|
e.style.removeProperty('height');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('left', '100vw');
|
||||||
|
e.style.removeProperty('right');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('top', '100vh');
|
||||||
|
e.style.removeProperty('left');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('bottom', '100vh');
|
||||||
|
e.style.removeProperty('top');
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
// Just peeking
|
||||||
|
e.style.setProperty('bottom', '99vh');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should wait for element to be visible recursively', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
const promise = page.waitForSelector('div#inner', {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
await page.setContent(
|
await page.setContent(
|
||||||
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
|
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
|
||||||
);
|
);
|
||||||
expect(divVisible).toBe(false);
|
const element = await page.evaluateHandle(() => {
|
||||||
await page.evaluate(() => {
|
return document.getElementsByTagName('div')[0]!;
|
||||||
return document.querySelector('div')?.style.removeProperty('display');
|
|
||||||
});
|
});
|
||||||
expect(divVisible).toBe(false);
|
await expect(
|
||||||
await page.evaluate(() => {
|
Promise.race([promise, createTimeout(40)])
|
||||||
return document
|
).resolves.toBeFalsy();
|
||||||
.querySelector('div')
|
await element.evaluate(e => {
|
||||||
?.style.removeProperty('visibility');
|
return e.style.removeProperty('display');
|
||||||
});
|
});
|
||||||
expect(await waitForSelector).toBe(true);
|
await expect(
|
||||||
expect(divVisible).toBe(true);
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
return e.style.removeProperty('visibility');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it('hidden should wait for visibility: hidden', async () => {
|
it('should wait for element to be hidden (visibility)', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divHidden = false;
|
const promise = page.waitForSelector('div', {hidden: true});
|
||||||
await page.setContent(`<div style='display: block;'></div>`);
|
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||||
const waitForSelector = page
|
const element = await page.evaluateHandle(() => {
|
||||||
.waitForSelector('div', {hidden: true})
|
return document.getElementsByTagName('div')[0]!;
|
||||||
.then(() => {
|
|
||||||
return (divHidden = true);
|
|
||||||
});
|
|
||||||
await page.waitForSelector('div'); // do a round trip
|
|
||||||
expect(divHidden).toBe(false);
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return document
|
|
||||||
.querySelector('div')
|
|
||||||
?.style.setProperty('visibility', 'hidden');
|
|
||||||
});
|
});
|
||||||
expect(await waitForSelector).toBe(true);
|
await expect(
|
||||||
expect(divHidden).toBe(true);
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
return e.style.setProperty('visibility', 'hidden');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it('hidden should wait for display: none', async () => {
|
it('should wait for element to be hidden (display)', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divHidden = false;
|
const promise = page.waitForSelector('div', {hidden: true});
|
||||||
await page.setContent(`<div style='display: block;'></div>`);
|
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||||
const waitForSelector = page
|
const element = await page.evaluateHandle(() => {
|
||||||
.waitForSelector('div', {hidden: true})
|
return document.getElementsByTagName('div')[0]!;
|
||||||
.then(() => {
|
|
||||||
return (divHidden = true);
|
|
||||||
});
|
|
||||||
await page.waitForSelector('div'); // do a round trip
|
|
||||||
expect(divHidden).toBe(false);
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return document
|
|
||||||
.querySelector('div')
|
|
||||||
?.style.setProperty('display', 'none');
|
|
||||||
});
|
});
|
||||||
expect(await waitForSelector).toBe(true);
|
await expect(
|
||||||
expect(divHidden).toBe(true);
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
return e.style.setProperty('display', 'none');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it('hidden should wait for removal', async () => {
|
it('should wait for element to be hidden (bounding box)', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
await page.setContent(`<div></div>`);
|
const promise = page.waitForSelector('div', {hidden: true});
|
||||||
let divRemoved = false;
|
await page.setContent('<div>text</div>');
|
||||||
const waitForSelector = page
|
const element = await page.evaluateHandle(() => {
|
||||||
.waitForSelector('div', {hidden: true})
|
return document.getElementsByTagName('div')[0]!;
|
||||||
.then(() => {
|
|
||||||
return (divRemoved = true);
|
|
||||||
});
|
|
||||||
await page.waitForSelector('div'); // do a round trip
|
|
||||||
expect(divRemoved).toBe(false);
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return document.querySelector('div')?.remove();
|
|
||||||
});
|
});
|
||||||
expect(await waitForSelector).toBe(true);
|
await expect(
|
||||||
expect(divRemoved).toBe(true);
|
Promise.race([promise, createTimeout(40)])
|
||||||
|
).resolves.toBeFalsy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.style.setProperty('height', '0');
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should wait for element to be hidden (removal)', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
const promise = page.waitForSelector('div', {hidden: true});
|
||||||
|
await page.setContent(`<div>text</div>`);
|
||||||
|
const element = await page.evaluateHandle(() => {
|
||||||
|
return document.getElementsByTagName('div')[0]!;
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
Promise.race([promise, createTimeout(40, true)])
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
await element.evaluate(e => {
|
||||||
|
e.remove();
|
||||||
|
});
|
||||||
|
await expect(promise).resolves.toBeFalsy();
|
||||||
});
|
});
|
||||||
it('should return null if waiting to hide non-existing element', async () => {
|
it('should return null if waiting to hide non-existing element', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
@ -650,13 +678,13 @@ describe('waittask specs', function () {
|
||||||
});
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain(
|
||||||
'waiting for selector `div` failed: timeout'
|
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
await page.setContent(`<div></div>`);
|
await page.setContent(`<div>text</div>`);
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page
|
await page
|
||||||
.waitForSelector('div', {hidden: true, timeout: 10})
|
.waitForSelector('div', {hidden: true, timeout: 10})
|
||||||
|
|
@ -665,7 +693,7 @@ describe('waittask specs', function () {
|
||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain(
|
||||||
'waiting for selector `div` to be hidden failed: timeout'
|
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -701,9 +729,11 @@ describe('waittask specs', function () {
|
||||||
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
|
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error?.stack).toContain('waiting for selector `.zombo` failed');
|
expect(error?.stack).toContain(
|
||||||
|
'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
|
||||||
|
);
|
||||||
// The extension is ts here as Mocha maps back via sourcemaps.
|
// The extension is ts here as Mocha maps back via sourcemaps.
|
||||||
expect(error?.stack).toContain('waittask.spec.ts');
|
expect(error?.stack).toContain('WaitTask.ts');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -733,9 +763,7 @@ describe('waittask specs', function () {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||||
'waiting for selector `.//div` failed: timeout 10ms exceeded'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should run in specified frame', async () => {
|
it('should run in specified frame', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
|
@ -772,7 +800,7 @@ describe('waittask specs', function () {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let divHidden = false;
|
let divHidden = false;
|
||||||
await page.setContent(`<div style='display: block;'></div>`);
|
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||||
const waitForXPath = page
|
const waitForXPath = page
|
||||||
.waitForXPath('//div', {hidden: true})
|
.waitForXPath('//div', {hidden: true})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,13 @@ import expect from 'expect';
|
||||||
import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
|
import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
|
||||||
import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js';
|
import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js';
|
||||||
import {
|
import {
|
||||||
describeFailsFirefox,
|
|
||||||
getTestState,
|
getTestState,
|
||||||
setupTestBrowserHooks,
|
setupTestBrowserHooks,
|
||||||
setupTestPageAndContextHooks,
|
setupTestPageAndContextHooks,
|
||||||
} from './mocha-utils.js';
|
} from './mocha-utils.js';
|
||||||
import {waitEvent} from './utils.js';
|
import {waitEvent} from './utils.js';
|
||||||
|
|
||||||
describeFailsFirefox('Workers', function () {
|
describe('Workers', function () {
|
||||||
setupTestBrowserHooks();
|
setupTestBrowserHooks();
|
||||||
setupTestPageAndContextHooks();
|
setupTestPageAndContextHooks();
|
||||||
it('Page.workers', async () => {
|
it('Page.workers', async () => {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"references": [
|
"references": [
|
||||||
{"path": "../tsconfig.lib.json"},
|
{"path": "../src/tsconfig.cjs.json"},
|
||||||
{"path": "../utils/testserver/tsconfig.json"}
|
{"path": "../utils/testserver/tsconfig.json"},
|
||||||
|
{"path": "../utils/mochaRunner/tsconfig.json"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
remote/test/puppeteer/utils/generate-matrix.js
Normal file
43
remote/test/puppeteer/utils/generate-matrix.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync('./test/TestSuites.json', 'utf-8'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} platform
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function mapPlatform(platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case 'linux':
|
||||||
|
return 'ubuntu-latest';
|
||||||
|
case 'win32':
|
||||||
|
return 'windows-latest';
|
||||||
|
case 'darwin':
|
||||||
|
return 'macos-latest';
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported platform');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const suite of data.testSuites) {
|
||||||
|
for (const platform of suite.platforms) {
|
||||||
|
if (platform === 'linux' && suite.id !== 'firefox-bidi') {
|
||||||
|
for (const node of [14, 16, 18]) {
|
||||||
|
result.push(`- name: ${suite.id}
|
||||||
|
machine: ${mapPlatform(platform)}
|
||||||
|
xvfb: true
|
||||||
|
node: ${node}
|
||||||
|
suite: ${suite.id}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(`- name: ${suite.id}
|
||||||
|
machine: ${mapPlatform(platform)}
|
||||||
|
xvfb: ${platform === 'linux'}
|
||||||
|
node: 18
|
||||||
|
suite: ${suite.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(result.join('\n'));
|
||||||
|
|
@ -6,7 +6,7 @@ import {sync as glob} from 'glob';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {job} from './internal/job.js';
|
import {job} from './internal/job.js';
|
||||||
|
|
||||||
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
|
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util', 'api'];
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await job('', async ({outputs}) => {
|
await job('', async ({outputs}) => {
|
||||||
|
|
@ -36,7 +36,7 @@ const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
|
||||||
outdir: tmp,
|
outdir: tmp,
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
target: 'ES2019',
|
target: 'ES2022',
|
||||||
});
|
});
|
||||||
const baseName = path.basename(input);
|
const baseName = path.basename(input);
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue