зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
b306cf161f
Коммит
d115e24d80
|
@ -126,6 +126,8 @@ _OPT\.OBJ/
|
|||
^remote/test/puppeteer/test/build
|
||||
^remote/test/puppeteer/test/output-firefox
|
||||
^remote/test/puppeteer/test/output-chromium
|
||||
^remote/test/puppeteer/testserver/lib/
|
||||
^remote/test/puppeteer/utils/mochaRunner/lib/
|
||||
^remote/test/puppeteer/website
|
||||
|
||||
# git checkout of libstagefright
|
||||
|
|
|
@ -15,4 +15,6 @@ test/puppeteer/src/generated
|
|||
test/puppeteer/test/build
|
||||
test/puppeteer/test/output-firefox
|
||||
test/puppeteer/test/output-chromium
|
||||
test/puppeteer/testserver/lib/
|
||||
test/puppeteer/utils/mochaRunner/lib/
|
||||
test/puppeteer/website
|
||||
|
|
|
@ -17,7 +17,6 @@ import mozprofile
|
|||
from mach.decorators import Command, CommandArgument, SubCommand
|
||||
from mozbuild import nodeutil
|
||||
from mozbuild.base import BinaryNotFoundException, MozbuildObject
|
||||
from six import iteritems
|
||||
|
||||
EX_CONFIG = 78
|
||||
EX_SOFTWARE = 70
|
||||
|
@ -261,8 +260,10 @@ class MochaOutputHandler(object):
|
|||
if not status and not test_start:
|
||||
return
|
||||
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_file_name = os.path.basename(test_path).replace(".js", "")
|
||||
test_err = test_info.get("err")
|
||||
if status == "FAIL" and test_err:
|
||||
if "timeout" in test_err.lower():
|
||||
|
@ -276,7 +277,32 @@ class MochaOutputHandler(object):
|
|||
if test_start:
|
||||
self.logger.test_start(test_name)
|
||||
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,
|
||||
# so if a test is disabled just expect that and note the unexpected skip
|
||||
# Also, mocha doesn't log test-start for skipped tests
|
||||
|
@ -308,34 +334,7 @@ class MochaOutputHandler(object):
|
|||
known_intermittent=known_intermittent,
|
||||
)
|
||||
|
||||
def new_expected(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,)
|
||||
)
|
||||
|
||||
def after_end(self):
|
||||
if self.unexpected_skips:
|
||||
self.has_unexpected = True
|
||||
for test_name in self.unexpected_skips:
|
||||
|
@ -392,8 +391,6 @@ class PuppeteerRunner(MozbuildObject):
|
|||
before invoking npm. Overrides default preferences.
|
||||
`enable_webrender`:
|
||||
Boolean to indicate whether to enable WebRender compositor in Gecko.
|
||||
`write_results`:
|
||||
Path to write the results json file
|
||||
`subset`
|
||||
Indicates only a subset of tests are being run, so we should
|
||||
skip the check for missing results
|
||||
|
@ -425,6 +422,7 @@ class PuppeteerRunner(MozbuildObject):
|
|||
"--timeout",
|
||||
"20000",
|
||||
"--no-parallel",
|
||||
"--no-coverage",
|
||||
]
|
||||
env["HEADLESS"] = str(params.get("headless", False))
|
||||
|
||||
|
@ -454,15 +452,31 @@ class PuppeteerRunner(MozbuildObject):
|
|||
env["EXTRA_LAUNCH_OPTIONS"] = json.dumps(extra_options)
|
||||
|
||||
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:
|
||||
expected_data = json.load(f)
|
||||
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(
|
||||
*command,
|
||||
cwd=self.puppeteer_dir,
|
||||
|
@ -476,7 +490,7 @@ class PuppeteerRunner(MozbuildObject):
|
|||
# failure, so use an output_timeout as a fallback
|
||||
wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False)
|
||||
|
||||
output_handler.after_end(params.get("subset", False))
|
||||
output_handler.after_end()
|
||||
|
||||
# Non-zero return codes are non-fatal for now since we have some
|
||||
# issues with unresolved promises that shouldn't otherwise block
|
||||
|
@ -484,12 +498,6 @@ class PuppeteerRunner(MozbuildObject):
|
|||
if proc.returncode != 0:
|
||||
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:
|
||||
exit(1, "Got unexpected results")
|
||||
|
||||
|
@ -547,18 +555,6 @@ def create_parser_puppeteer():
|
|||
"debug level messages with -v, trace messages with -vv,"
|
||||
"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(
|
||||
"--subset",
|
||||
action="store_true",
|
||||
|
@ -597,7 +593,6 @@ def puppeteer_test(
|
|||
verbosity=0,
|
||||
tests=None,
|
||||
product="firefox",
|
||||
write_results=None,
|
||||
subset=False,
|
||||
**kwargs,
|
||||
):
|
||||
|
@ -657,7 +652,6 @@ def puppeteer_test(
|
|||
"extra_prefs": prefs,
|
||||
"product": product,
|
||||
"extra_launcher_options": options,
|
||||
"write_results": write_results,
|
||||
"subset": subset,
|
||||
}
|
||||
puppeteer = command_context._spawn(PuppeteerRunner)
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -18,10 +18,10 @@ module.exports = {
|
|||
reporter: 'dot',
|
||||
logLevel: 'debug',
|
||||
require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
|
||||
spec: 'test/build/*.spec.js',
|
||||
spec: 'test/build/**/*.spec.js',
|
||||
exit: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
parallel: !!process.env.PARALLEL,
|
||||
timeout: 25 * 1000,
|
||||
timeout: 25_000,
|
||||
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.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ to the project.
|
|||
|
||||
function JSONExtra(runner, options) {
|
||||
mocha.reporters.Base.call(this, runner, options);
|
||||
mocha.reporters.JSON.call(this, runner, options);
|
||||
const self = this;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
schema: 1
|
||||
bugzilla:
|
||||
component: Agent
|
||||
product: Remote Protocol
|
||||
|
@ -6,5 +5,6 @@ origin:
|
|||
description: Headless Chrome Node API
|
||||
license: Apache-2.0
|
||||
name: puppeteer
|
||||
release: 0d2d99efeca73fba255fb10b28b5d3f50c2e20e4
|
||||
url: https://github.com/puppeteer/puppeteer
|
||||
release: 7d6927209e5d557891bd618ddb01d54bc3566307
|
||||
url: /Users/alexandraborovova/Projects/puppeteer
|
||||
schema: 1
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "puppeteer",
|
||||
"version": "17.1.2",
|
||||
"version": "18.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "puppeteer",
|
||||
"version": "17.1.2",
|
||||
"version": "18.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -80,7 +80,8 @@
|
|||
"text-diff": "1.0.1",
|
||||
"tsd": "0.22.0",
|
||||
"tsx": "3.8.2",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "4.7.4",
|
||||
"zod": "3.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.1.0"
|
||||
|
@ -7809,6 +7810,15 @@
|
|||
"optionalDependencies": {
|
||||
"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": {
|
||||
|
@ -13501,6 +13511,12 @@
|
|||
"lodash.isequal": "^4.5.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",
|
||||
"version": "17.1.2",
|
||||
"version": "18.0.0",
|
||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||
"keywords": [
|
||||
"puppeteer",
|
||||
|
@ -27,14 +27,14 @@
|
|||
"node": ">=14.1.0"
|
||||
},
|
||||
"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: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:headless": "cross-env HEADLESS=true PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
||||
"test:chrome:headless-chrome": "cross-env HEADLESS=chrome PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
||||
"test:chrome:headful": "cross-env HEADLESS=false PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
|
||||
"test:chrome:headless": "npm run test -- --test-suite chrome-headless",
|
||||
"test:chrome:headless-chrome": "npm run test -- --test-suite chrome-new-headless",
|
||||
"test:chrome:headful": "npm run test -- --test-suite chrome-headful",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prepare": "node typescript-if-required.js && husky install",
|
||||
"lint": "run-s lint:prettier lint:eslint",
|
||||
|
@ -139,6 +139,7 @@
|
|||
"text-diff": "1.0.1",
|
||||
"tsd": "0.22.0",
|
||||
"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}"
|
||||
node --input-type="module" --eval="import puppeteer from 'puppeteer-core'"
|
||||
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));
|
||||
})();
|
||||
"
|
||||
|
|
|
@ -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 {ElementHandle} from './ElementHandle.js';
|
||||
import {Frame} from './Frame.js';
|
||||
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||
import {InternalQueryHandler} from './QueryHandler.js';
|
||||
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||
import {PuppeteerQueryHandler} from './QueryHandler.js';
|
||||
|
||||
async function queryAXTree(
|
||||
client: CDPSession,
|
||||
|
@ -95,7 +95,7 @@ const queryOneId = async (element: ElementHandle<Node>, selector: string) => {
|
|||
return res[0].backendDOMNodeId;
|
||||
};
|
||||
|
||||
const queryOne: InternalQueryHandler['queryOne'] = async (
|
||||
const queryOne: PuppeteerQueryHandler['queryOne'] = async (
|
||||
element,
|
||||
selector
|
||||
) => {
|
||||
|
@ -108,7 +108,7 @@ const queryOne: InternalQueryHandler['queryOne'] = async (
|
|||
)) as ElementHandle<Node>;
|
||||
};
|
||||
|
||||
const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||
const waitFor: PuppeteerQueryHandler['waitFor'] = async (
|
||||
elementOrFrame,
|
||||
selector,
|
||||
options
|
||||
|
@ -121,21 +121,20 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
|||
frame = elementOrFrame.frame;
|
||||
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
||||
}
|
||||
const binding: PageBinding = {
|
||||
name: 'ariaQuerySelector',
|
||||
pptrFunction: async (selector: string) => {
|
||||
const id = await queryOneId(
|
||||
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
||||
selector
|
||||
);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
||||
id
|
||||
)) as ElementHandle<Node>;
|
||||
},
|
||||
|
||||
const ariaQuerySelector = async (selector: string) => {
|
||||
const id = await queryOneId(
|
||||
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
||||
selector
|
||||
);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
||||
id
|
||||
)) as ElementHandle<Node>;
|
||||
};
|
||||
|
||||
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
||||
(_: Element, selector: string) => {
|
||||
return (
|
||||
|
@ -147,22 +146,19 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
|||
element,
|
||||
selector,
|
||||
options,
|
||||
binding
|
||||
new Set([ariaQuerySelector])
|
||||
);
|
||||
if (element) {
|
||||
await element.dispose();
|
||||
}
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
if (!(result instanceof ElementHandle)) {
|
||||
await result.dispose();
|
||||
await result?.dispose();
|
||||
return null;
|
||||
}
|
||||
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
||||
};
|
||||
|
||||
const queryAll: InternalQueryHandler['queryAll'] = async (
|
||||
const queryAll: PuppeteerQueryHandler['queryAll'] = async (
|
||||
element,
|
||||
selector
|
||||
) => {
|
||||
|
@ -182,7 +178,7 @@ const queryAll: InternalQueryHandler['queryAll'] = async (
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const ariaHandler: InternalQueryHandler = {
|
||||
export const ariaHandler: PuppeteerQueryHandler = {
|
||||
queryOne,
|
||||
waitFor,
|
||||
queryAll,
|
||||
|
|
|
@ -18,7 +18,6 @@ import {ChildProcess} from 'child_process';
|
|||
import {Protocol} from 'devtools-protocol';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
|
||||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {waitWithTimeout} from './util.js';
|
||||
import {Page} from './Page.js';
|
||||
import {Viewport} from './PuppeteerViewport.js';
|
||||
|
@ -27,196 +26,24 @@ import {TaskQueue} from './TaskQueue.js';
|
|||
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
|
||||
import {ChromeTargetManager} from './ChromeTargetManager.js';
|
||||
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
|
||||
|
||||
/**
|
||||
* 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 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<
|
||||
import {
|
||||
Browser as BrowserBase,
|
||||
BrowserContext,
|
||||
BrowserCloseCallback,
|
||||
TargetFilterCallback,
|
||||
IsPageTargetCallback,
|
||||
BrowserEmittedEvents,
|
||||
BrowserContextEmittedEvents,
|
||||
BrowserContextOptions,
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
||||
WaitForTargetOptions,
|
||||
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'],
|
||||
]);
|
||||
} from '../api/Browser.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
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 {
|
||||
export class CDPBrowser extends BrowserBase {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -230,8 +57,8 @@ export class Browser extends EventEmitter {
|
|||
closeCallback?: BrowserCloseCallback,
|
||||
targetFilterCallback?: TargetFilterCallback,
|
||||
isPageTargetCallback?: IsPageTargetCallback
|
||||
): Promise<Browser> {
|
||||
const browser = new Browser(
|
||||
): Promise<CDPBrowser> {
|
||||
const browser = new CDPBrowser(
|
||||
product,
|
||||
connection,
|
||||
contextIds,
|
||||
|
@ -252,15 +79,15 @@ export class Browser extends EventEmitter {
|
|||
#closeCallback: BrowserCloseCallback;
|
||||
#targetFilterCallback: TargetFilterCallback;
|
||||
#isPageTargetCallback!: IsPageTargetCallback;
|
||||
#defaultContext: BrowserContext;
|
||||
#contexts: Map<string, BrowserContext>;
|
||||
#defaultContext: CDPBrowserContext;
|
||||
#contexts: Map<string, CDPBrowserContext>;
|
||||
#screenshotTaskQueue: TaskQueue;
|
||||
#targetManager: TargetManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _targets(): Map<string, Target> {
|
||||
override get _targets(): Map<string, Target> {
|
||||
return this.#targetManager.getAvailableTargets();
|
||||
}
|
||||
|
||||
|
@ -305,12 +132,12 @@ export class Browser extends EventEmitter {
|
|||
this.#targetFilterCallback
|
||||
);
|
||||
}
|
||||
this.#defaultContext = new BrowserContext(this.#connection, this);
|
||||
this.#defaultContext = new CDPBrowserContext(this.#connection, this);
|
||||
this.#contexts = new Map();
|
||||
for (const contextId of contextIds) {
|
||||
this.#contexts.set(
|
||||
contextId,
|
||||
new BrowserContext(this.#connection, this, contextId)
|
||||
new CDPBrowserContext(this.#connection, this, contextId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +149,7 @@ export class Browser extends EventEmitter {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _attach(): Promise<void> {
|
||||
override async _attach(): Promise<void> {
|
||||
this.#connection.on(
|
||||
ConnectionEmittedEvents.Disconnected,
|
||||
this.#emitDisconnected
|
||||
|
@ -349,7 +176,7 @@ export class Browser extends EventEmitter {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
_detach(): void {
|
||||
override _detach(): void {
|
||||
this.#connection.off(
|
||||
ConnectionEmittedEvents.Disconnected,
|
||||
this.#emitDisconnected
|
||||
|
@ -376,7 +203,7 @@ export class Browser extends EventEmitter {
|
|||
* The spawned browser process. Returns `null` if the browser instance was created with
|
||||
* {@link Puppeteer.connect}.
|
||||
*/
|
||||
process(): ChildProcess | null {
|
||||
override process(): ChildProcess | null {
|
||||
return this.#process ?? null;
|
||||
}
|
||||
|
||||
|
@ -402,7 +229,7 @@ export class Browser extends EventEmitter {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
||||
override _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
||||
return this.#isPageTargetCallback;
|
||||
}
|
||||
|
||||
|
@ -424,9 +251,9 @@ export class Browser extends EventEmitter {
|
|||
* })();
|
||||
* ```
|
||||
*/
|
||||
async createIncognitoBrowserContext(
|
||||
override async createIncognitoBrowserContext(
|
||||
options: BrowserContextOptions = {}
|
||||
): Promise<BrowserContext> {
|
||||
): Promise<CDPBrowserContext> {
|
||||
const {proxyServer, proxyBypassList} = options;
|
||||
|
||||
const {browserContextId} = await this.#connection.send(
|
||||
|
@ -436,7 +263,7 @@ export class Browser extends EventEmitter {
|
|||
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
|
||||
}
|
||||
);
|
||||
const context = new BrowserContext(
|
||||
const context = new CDPBrowserContext(
|
||||
this.#connection,
|
||||
this,
|
||||
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
|
||||
* return a single instance of {@link BrowserContext}.
|
||||
*/
|
||||
browserContexts(): BrowserContext[] {
|
||||
override browserContexts(): CDPBrowserContext[] {
|
||||
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default browser context. The default browser context cannot be closed.
|
||||
*/
|
||||
defaultBrowserContext(): BrowserContext {
|
||||
override defaultBrowserContext(): CDPBrowserContext {
|
||||
return this.#defaultContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _disposeContext(contextId?: string): Promise<void> {
|
||||
override async _disposeContext(contextId?: string): Promise<void> {
|
||||
if (!contextId) {
|
||||
return;
|
||||
}
|
||||
|
@ -564,7 +391,7 @@ export class Browser extends EventEmitter {
|
|||
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
|
||||
* | browser endpoint}.
|
||||
*/
|
||||
wsEndpoint(): string {
|
||||
override wsEndpoint(): string {
|
||||
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
|
||||
* a default browser context.
|
||||
*/
|
||||
async newPage(): Promise<Page> {
|
||||
override async newPage(): Promise<Page> {
|
||||
return this.#defaultContext.newPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _createPageInContext(contextId?: string): Promise<Page> {
|
||||
override async _createPageInContext(contextId?: string): Promise<Page> {
|
||||
const {targetId} = await this.#connection.send('Target.createTarget', {
|
||||
url: 'about:blank',
|
||||
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
|
||||
* an array with all the targets in all browser contexts.
|
||||
*/
|
||||
targets(): Target[] {
|
||||
override targets(): Target[] {
|
||||
return Array.from(
|
||||
this.#targetManager.getAvailableTargets().values()
|
||||
).filter(target => {
|
||||
|
@ -616,7 +443,7 @@ export class Browser extends EventEmitter {
|
|||
/**
|
||||
* The target associated with the browser.
|
||||
*/
|
||||
target(): Target {
|
||||
override target(): Target {
|
||||
const browserTarget = this.targets().find(target => {
|
||||
return target.type() === 'browser';
|
||||
});
|
||||
|
@ -643,7 +470,7 @@ export class Browser extends EventEmitter {
|
|||
* );
|
||||
* ```
|
||||
*/
|
||||
async waitForTarget(
|
||||
override async waitForTarget(
|
||||
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||
options: WaitForTargetOptions = {}
|
||||
): Promise<Target> {
|
||||
|
@ -683,7 +510,7 @@ export class Browser extends EventEmitter {
|
|||
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
|
||||
* here. You can find them using {@link Target.page}.
|
||||
*/
|
||||
async pages(): Promise<Page[]> {
|
||||
override async pages(): Promise<Page[]> {
|
||||
const contextPages = await Promise.all(
|
||||
this.browserContexts().map(context => {
|
||||
return context.pages();
|
||||
|
@ -705,7 +532,7 @@ export class Browser extends EventEmitter {
|
|||
*
|
||||
* 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();
|
||||
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
|
||||
* {@link Page.setUserAgent}.
|
||||
*/
|
||||
async userAgent(): Promise<string> {
|
||||
override async userAgent(): Promise<string> {
|
||||
const version = await this.#getVersion();
|
||||
return version.userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Closes Chromium and all of its pages (if any were opened). The
|
||||
* {@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);
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
disconnect(): void {
|
||||
override disconnect(): void {
|
||||
this.#targetManager.dispose();
|
||||
this.#connection.dispose();
|
||||
}
|
||||
|
@ -741,7 +569,7 @@ export class Browser extends EventEmitter {
|
|||
/**
|
||||
* Indicates that the browser is connected.
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
override isConnected(): boolean {
|
||||
return !this.#connection._closed;
|
||||
}
|
||||
|
||||
|
@ -749,75 +577,19 @@ export class Browser extends EventEmitter {
|
|||
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
|
||||
* 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
|
||||
* @internal
|
||||
*/
|
||||
export class BrowserContext extends EventEmitter {
|
||||
export class CDPBrowserContext extends BrowserContext {
|
||||
#connection: Connection;
|
||||
#browser: Browser;
|
||||
#browser: CDPBrowser;
|
||||
#id?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(connection: Connection, browser: Browser, contextId?: string) {
|
||||
constructor(connection: Connection, browser: CDPBrowser, contextId?: string) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#browser = browser;
|
||||
|
@ -827,7 +599,7 @@ export class BrowserContext extends EventEmitter {
|
|||
/**
|
||||
* An array of all active targets inside the browser context.
|
||||
*/
|
||||
targets(): Target[] {
|
||||
override targets(): Target[] {
|
||||
return this.#browser.targets().filter(target => {
|
||||
return target.browserContext() === this;
|
||||
});
|
||||
|
@ -853,7 +625,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* @returns Promise which resolves to the first target found
|
||||
* that matches the `predicate` function.
|
||||
*/
|
||||
waitForTarget(
|
||||
override waitForTarget(
|
||||
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||
options: {timeout?: number} = {}
|
||||
): Promise<Target> {
|
||||
|
@ -869,7 +641,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* Non visible pages, such as `"background_page"`, will not be listed here.
|
||||
* 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(
|
||||
this.targets()
|
||||
.filter(target => {
|
||||
|
@ -897,7 +669,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* @remarks
|
||||
* The default browser context cannot be closed.
|
||||
*/
|
||||
isIncognito(): boolean {
|
||||
override isIncognito(): boolean {
|
||||
return !!this.#id;
|
||||
}
|
||||
|
||||
|
@ -915,7 +687,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* @param permissions - An array of permissions to grant.
|
||||
* All permissions that are not listed here will be automatically denied.
|
||||
*/
|
||||
async overridePermissions(
|
||||
override async overridePermissions(
|
||||
origin: string,
|
||||
permissions: Permission[]
|
||||
): Promise<void> {
|
||||
|
@ -946,7 +718,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* context.clearPermissionOverrides();
|
||||
* ```
|
||||
*/
|
||||
async clearPermissionOverrides(): Promise<void> {
|
||||
override async clearPermissionOverrides(): Promise<void> {
|
||||
await this.#connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this.#id || undefined,
|
||||
});
|
||||
|
@ -955,14 +727,14 @@ export class BrowserContext extends EventEmitter {
|
|||
/**
|
||||
* Creates a new page in the browser context.
|
||||
*/
|
||||
newPage(): Promise<Page> {
|
||||
override newPage(): Promise<Page> {
|
||||
return this.#browser._createPageInContext(this.#id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The browser this browser context belongs to.
|
||||
*/
|
||||
browser(): Browser {
|
||||
override browser(): CDPBrowser {
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
|
@ -973,7 +745,7 @@ export class BrowserContext extends EventEmitter {
|
|||
* @remarks
|
||||
* 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!');
|
||||
await this.#browser._disposeContext(this.#id);
|
||||
}
|
||||
|
|
|
@ -18,11 +18,8 @@ import {debugError} from './util.js';
|
|||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {isNode} from '../environment.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {
|
||||
Browser,
|
||||
IsPageTargetCallback,
|
||||
TargetFilterCallback,
|
||||
} from './Browser.js';
|
||||
import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
|
||||
import {CDPBrowser} from './Browser.js';
|
||||
import {Connection} from './Connection.js';
|
||||
import {ConnectionTransport} from './ConnectionTransport.js';
|
||||
import {getFetch} from './fetch.js';
|
||||
|
@ -55,6 +52,11 @@ export interface BrowserConnectOptions {
|
|||
* @internal
|
||||
*/
|
||||
_isPageTarget?: IsPageTargetCallback;
|
||||
/**
|
||||
* @defaultValue 'cdp'
|
||||
* @internal
|
||||
*/
|
||||
protocol?: 'cdp' | 'webDriverBiDi';
|
||||
}
|
||||
|
||||
const getWebSocketTransportClass = async () => {
|
||||
|
@ -70,13 +72,13 @@ const getWebSocketTransportClass = async () => {
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
export async function _connectToBrowser(
|
||||
export async function _connectToCDPBrowser(
|
||||
options: BrowserConnectOptions & {
|
||||
browserWSEndpoint?: string;
|
||||
browserURL?: string;
|
||||
transport?: ConnectionTransport;
|
||||
}
|
||||
): Promise<Browser> {
|
||||
): Promise<CDPBrowser> {
|
||||
const {
|
||||
browserWSEndpoint,
|
||||
browserURL,
|
||||
|
@ -118,7 +120,7 @@ export async function _connectToBrowser(
|
|||
const {browserContextIds} = await connection.send(
|
||||
'Target.getBrowserContexts'
|
||||
);
|
||||
const browser = await Browser._create(
|
||||
const browser = await CDPBrowser._create(
|
||||
product || 'chrome',
|
||||
connection,
|
||||
browserContextIds,
|
||||
|
@ -131,7 +133,6 @@ export async function _connectToBrowser(
|
|||
targetFilter,
|
||||
isPageTarget
|
||||
);
|
||||
await browser.pages();
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import {CDPSession, Connection} from './Connection.js';
|
|||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {Target} from './Target.js';
|
||||
import {debugError} from './util.js';
|
||||
import {TargetFilterCallback} from './Browser.js';
|
||||
import {TargetFilterCallback} from '../api/Browser.js';
|
||||
import {
|
||||
TargetInterceptor,
|
||||
TargetFactory,
|
||||
|
|
|
@ -56,7 +56,7 @@ export class Connection extends EventEmitter {
|
|||
#transport: ConnectionTransport;
|
||||
#delay: number;
|
||||
#lastId = 0;
|
||||
#sessions: Map<string, CDPSession> = new Map();
|
||||
#sessions: Map<string, CDPSessionImpl> = new Map();
|
||||
#closed = false;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#manuallyAttached = new Set<string>();
|
||||
|
@ -147,7 +147,7 @@ export class Connection extends EventEmitter {
|
|||
const object = JSON.parse(message);
|
||||
if (object.method === 'Target.attachedToTarget') {
|
||||
const sessionId = object.params.sessionId;
|
||||
const session = new CDPSession(
|
||||
const session = new CDPSessionImpl(
|
||||
this,
|
||||
object.params.targetInfo.type,
|
||||
sessionId
|
||||
|
@ -310,6 +310,47 @@ export const CDPSessionEmittedEvents = {
|
|||
* @public
|
||||
*/
|
||||
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;
|
||||
#targetType: string;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
|
@ -325,11 +366,11 @@ export class CDPSession extends EventEmitter {
|
|||
this.#sessionId = sessionId;
|
||||
}
|
||||
|
||||
connection(): Connection | undefined {
|
||||
override connection(): Connection | undefined {
|
||||
return this.#connection;
|
||||
}
|
||||
|
||||
send<T extends keyof ProtocolMapping.Commands>(
|
||||
override send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): 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
|
||||
* 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) {
|
||||
throw new Error(
|
||||
`Session already detached. Most likely the ${
|
||||
|
@ -419,7 +460,7 @@ export class CDPSession extends EventEmitter {
|
|||
/**
|
||||
* Returns the session's id.
|
||||
*/
|
||||
id(): string {
|
||||
override id(): string {
|
||||
return this.#sessionId;
|
||||
}
|
||||
}
|
||||
|
@ -445,3 +486,13 @@ function rewriteError(
|
|||
error.originalMessage = originalMessage ?? error.originalMessage;
|
||||
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
|
||||
* scripts that are dynamically created on the page using `eval` or
|
||||
* `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> {
|
||||
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 {assert} from '../util/assert.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, {
|
||||
Emitter,
|
||||
EventType,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
|
|||
import {CDPSession} from './Connection.js';
|
||||
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {JSHandle} from './JSHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
import {EvaluateFunc, HandleFor} from './types.js';
|
||||
import {
|
||||
createJSHandle,
|
||||
|
@ -273,7 +274,7 @@ export class ExecutionContext {
|
|||
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: functionText + '\n' + suffix + '\n',
|
||||
executionContextId: this._contextId,
|
||||
arguments: args.map(convertArgument.bind(this)),
|
||||
arguments: await Promise.all(args.map(convertArgument.bind(this))),
|
||||
returnByValue,
|
||||
awaitPromise: true,
|
||||
userGesture: true,
|
||||
|
@ -298,10 +299,13 @@ export class ExecutionContext {
|
|||
? valueFromRemoteObject(remoteObject)
|
||||
: createJSHandle(this, remoteObject);
|
||||
|
||||
function convertArgument(
|
||||
async function convertArgument(
|
||||
this: ExecutionContext,
|
||||
arg: unknown
|
||||
): Protocol.Runtime.CallArgument {
|
||||
): Promise<Protocol.Runtime.CallArgument> {
|
||||
if (arg instanceof LazyArg) {
|
||||
arg = await arg.get();
|
||||
}
|
||||
if (typeof arg === 'bigint') {
|
||||
// eslint-disable-line valid-typeof
|
||||
return {unserializableValue: `${arg.toString()}n`};
|
||||
|
|
|
@ -18,7 +18,7 @@ import Protocol from 'devtools-protocol';
|
|||
import {assert} from '../util/assert.js';
|
||||
import {CDPSession, Connection} from './Connection.js';
|
||||
import {Target} from './Target.js';
|
||||
import {TargetFilterCallback} from './Browser.js';
|
||||
import {TargetFilterCallback} from '../api/Browser.js';
|
||||
import {
|
||||
TargetFactory,
|
||||
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 {assert} from '../util/assert.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
@ -36,7 +52,7 @@ export interface FrameWaitForFunctionOptions {
|
|||
*
|
||||
* - `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).
|
||||
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
||||
|
@ -150,7 +166,6 @@ export interface FrameAddStyleTagOptions {
|
|||
* @public
|
||||
*/
|
||||
export class Frame {
|
||||
#parentFrame: Frame | null;
|
||||
#url = '';
|
||||
#detached = false;
|
||||
#client!: CDPSession;
|
||||
|
@ -186,30 +201,25 @@ export class Frame {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
_childFrames: Set<Frame>;
|
||||
_parentId?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
frameManager: FrameManager,
|
||||
parentFrame: Frame | null,
|
||||
frameId: string,
|
||||
parentFrameId: string | undefined,
|
||||
client: CDPSession
|
||||
) {
|
||||
this._frameManager = frameManager;
|
||||
this.#parentFrame = parentFrame ?? null;
|
||||
this.#url = '';
|
||||
this._id = frameId;
|
||||
this._parentId = parentFrameId;
|
||||
this.#detached = false;
|
||||
|
||||
this._loaderId = '';
|
||||
|
||||
this._childFrames = new Set();
|
||||
if (this.#parentFrame) {
|
||||
this.#parentFrame._childFrames.add(this);
|
||||
}
|
||||
|
||||
this.updateClient(client);
|
||||
}
|
||||
|
||||
|
@ -220,7 +230,7 @@ export class Frame {
|
|||
this.#client = client;
|
||||
this.worlds = {
|
||||
[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 = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
// TODO: Fix when NodeHandle has been added.
|
||||
return this.worlds[MAIN_WORLD].waitForFunction(
|
||||
pageFunction,
|
||||
options,
|
||||
|
@ -721,14 +730,14 @@ export class Frame {
|
|||
* @returns The parent frame, if any. Detached and main frames return `null`.
|
||||
*/
|
||||
parentFrame(): Frame | null {
|
||||
return this.#parentFrame;
|
||||
return this._frameManager._frameTree.parentFrame(this._id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns An array of child frames.
|
||||
*/
|
||||
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(
|
||||
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
||||
async ({url, id, type, content}) => {
|
||||
const promise = InjectedUtil.createDeferredPromise<void>();
|
||||
async ({createDeferredPromise}, {url, id, type, content}) => {
|
||||
const promise = createDeferredPromise<void>();
|
||||
const script = document.createElement('script');
|
||||
script.type = type;
|
||||
script.text = content;
|
||||
|
@ -809,6 +818,7 @@ export class Frame {
|
|||
await promise;
|
||||
return script;
|
||||
},
|
||||
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
|
||||
{...options, type, content}
|
||||
)
|
||||
);
|
||||
|
@ -858,8 +868,8 @@ export class Frame {
|
|||
|
||||
return this.worlds[MAIN_WORLD].transferHandle(
|
||||
await this.worlds[PUPPETEER_WORLD].evaluateHandle(
|
||||
async ({url, content}) => {
|
||||
const promise = InjectedUtil.createDeferredPromise<void>();
|
||||
async ({createDeferredPromise}, {url, content}) => {
|
||||
const promise = createDeferredPromise<void>();
|
||||
let element: HTMLStyleElement | HTMLLinkElement;
|
||||
if (!url) {
|
||||
element = document.createElement('style');
|
||||
|
@ -892,6 +902,7 @@ export class Frame {
|
|||
await promise;
|
||||
return element;
|
||||
},
|
||||
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
|
||||
options
|
||||
)
|
||||
);
|
||||
|
@ -1089,9 +1100,5 @@ export class Frame {
|
|||
this.#detached = true;
|
||||
this.worlds[MAIN_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 {assert} from '../util/assert.js';
|
||||
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
|
||||
import {DeferredPromise} from '../util/DeferredPromise.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {CDPSession, isTargetClosedError} from './Connection.js';
|
||||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
|
||||
import {Frame} from './Frame.js';
|
||||
import {FrameTree} from './FrameTree.js';
|
||||
import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||
import {NetworkManager} from './NetworkManager.js';
|
||||
import {Page} from './Page.js';
|
||||
|
@ -60,20 +59,13 @@ export class FrameManager extends EventEmitter {
|
|||
#page: Page;
|
||||
#networkManager: NetworkManager;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#frames = new Map<string, Frame>();
|
||||
#contextIdToContext = new Map<string, ExecutionContext>();
|
||||
#isolatedWorlds = new Set<string>();
|
||||
#mainFrame?: Frame;
|
||||
#client: CDPSession;
|
||||
/**
|
||||
* Keeps track of OOPIF targets/frames (target ID == frame ID for OOPIFs)
|
||||
* that are being initialized.
|
||||
* @internal
|
||||
*/
|
||||
#framesPendingTargetInit = new Map<string, DeferredPromise<void>>();
|
||||
/**
|
||||
* Keeps track of frames that are in the process of being attached in #onFrameAttached.
|
||||
*/
|
||||
#framesPendingAttachment = new Map<string, DeferredPromise<void>>();
|
||||
_frameTree = new FrameTree();
|
||||
|
||||
get timeoutSettings(): TimeoutSettings {
|
||||
return this.#timeoutSettings;
|
||||
|
@ -140,19 +132,8 @@ export class FrameManager extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
async initialize(
|
||||
targetId: string,
|
||||
client: CDPSession = this.#client
|
||||
): Promise<void> {
|
||||
async initialize(client: CDPSession = this.#client): Promise<void> {
|
||||
try {
|
||||
if (!this.#framesPendingTargetInit.has(targetId)) {
|
||||
this.#framesPendingTargetInit.set(
|
||||
targetId,
|
||||
createDebuggableDeferredPromise(
|
||||
`Waiting for target frame ${targetId} failed`
|
||||
)
|
||||
);
|
||||
}
|
||||
const result = await Promise.all([
|
||||
client.send('Page.enable'),
|
||||
client.send('Page.getFrameTree'),
|
||||
|
@ -172,18 +153,11 @@ export class FrameManager extends EventEmitter {
|
|||
]);
|
||||
} catch (error) {
|
||||
// The target might have been closed before the initialization finished.
|
||||
if (
|
||||
isErrorLike(error) &&
|
||||
(error.message.includes('Target closed') ||
|
||||
error.message.includes('Session closed'))
|
||||
) {
|
||||
if (isErrorLike(error) && isTargetClosedError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
this.#framesPendingTargetInit.get(targetId)?.resolve();
|
||||
this.#framesPendingTargetInit.delete(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,16 +176,17 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
|
||||
mainFrame(): Frame {
|
||||
assert(this.#mainFrame, 'Requesting main frame too early!');
|
||||
return this.#mainFrame;
|
||||
const mainFrame = this._frameTree.getMainFrame();
|
||||
assert(mainFrame, 'Requesting main frame too early!');
|
||||
return mainFrame;
|
||||
}
|
||||
|
||||
frames(): Frame[] {
|
||||
return Array.from(this.#frames.values());
|
||||
return Array.from(this._frameTree.frames());
|
||||
}
|
||||
|
||||
frame(frameId: string): Frame | null {
|
||||
return this.#frames.get(frameId) || null;
|
||||
return this._frameTree.getById(frameId) || null;
|
||||
}
|
||||
|
||||
onAttachedToTarget(target: Target): void {
|
||||
|
@ -219,16 +194,16 @@ export class FrameManager extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const frame = this.#frames.get(target._getTargetInfo().targetId);
|
||||
const frame = this.frame(target._getTargetInfo().targetId);
|
||||
if (frame) {
|
||||
frame.updateClient(target._session()!);
|
||||
}
|
||||
this.setupEventListeners(target._session()!);
|
||||
this.initialize(target._getTargetInfo().targetId, target._session());
|
||||
this.initialize(target._session());
|
||||
}
|
||||
|
||||
onDetachedFromTarget(target: Target): void {
|
||||
const frame = this.#frames.get(target._targetId);
|
||||
const frame = this.frame(target._targetId);
|
||||
if (frame && frame.isOOPFrame()) {
|
||||
// When an OOP iframe is removed from the page, it
|
||||
// will only get a Target.detachedFromTarget event.
|
||||
|
@ -237,7 +212,7 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
|
||||
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
||||
const frame = this.#frames.get(event.frameId);
|
||||
const frame = this.frame(event.frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
@ -246,7 +221,7 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
|
||||
#onFrameStartedLoading(frameId: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
@ -254,7 +229,7 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
|
||||
#onFrameStoppedLoading(frameId: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
@ -288,8 +263,8 @@ export class FrameManager extends EventEmitter {
|
|||
frameId: string,
|
||||
parentFrameId: string
|
||||
): void {
|
||||
if (this.#frames.has(frameId)) {
|
||||
const frame = this.#frames.get(frameId)!;
|
||||
let frame = this.frame(frameId);
|
||||
if (frame) {
|
||||
if (session && frame.isOOPFrame()) {
|
||||
// If an OOP iframes becomes a normal iframe again
|
||||
// it is first attached to the parent page before
|
||||
|
@ -298,86 +273,41 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const parentFrame = this.#frames.get(parentFrameId);
|
||||
|
||||
const complete = (parentFrame: Frame) => {
|
||||
assert(parentFrame, `Parent frame ${parentFrameId} not found`);
|
||||
const frame = new Frame(this, parentFrame, frameId, session);
|
||||
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`);
|
||||
frame = new Frame(this, frameId, parentFrameId, session);
|
||||
this._frameTree.addFrame(frame);
|
||||
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
|
||||
}
|
||||
|
||||
#onFrameNavigated(framePayload: Protocol.Page.Frame): void {
|
||||
async #onFrameNavigated(framePayload: Protocol.Page.Frame): Promise<void> {
|
||||
const frameId = framePayload.id;
|
||||
const isMainFrame = !framePayload.parentId;
|
||||
const frame = isMainFrame ? this.#mainFrame : this.#frames.get(frameId);
|
||||
|
||||
const complete = (frame?: Frame) => {
|
||||
assert(
|
||||
isMainFrame || frame,
|
||||
`Missing frame isMainFrame=${isMainFrame}, frameId=${frameId}`
|
||||
);
|
||||
let frame = this._frameTree.getById(frameId);
|
||||
|
||||
// Detach all child frames first.
|
||||
if (frame) {
|
||||
for (const child of frame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
// Detach all child frames first.
|
||||
if (frame) {
|
||||
for (const child of frame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
|
||||
// Update or create main frame.
|
||||
if (isMainFrame) {
|
||||
if (frame) {
|
||||
// Update frame id to retain frame identity on cross-process navigation.
|
||||
this.#frames.delete(frame._id);
|
||||
frame._id = 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> {
|
||||
|
@ -414,7 +344,7 @@ export class FrameManager extends EventEmitter {
|
|||
}
|
||||
|
||||
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
const frame = this.frame(frameId);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
@ -427,7 +357,7 @@ export class FrameManager extends EventEmitter {
|
|||
frameId: string,
|
||||
reason: Protocol.Page.FrameDetachedEventReason
|
||||
): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
const frame = this.frame(frameId);
|
||||
if (reason === 'remove') {
|
||||
// Only remove the frame if the reason for the detached event is
|
||||
// an actual removement of the frame.
|
||||
|
@ -446,8 +376,7 @@ export class FrameManager extends EventEmitter {
|
|||
): void {
|
||||
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
|
||||
const frameId = auxData && auxData.frameId;
|
||||
const frame =
|
||||
typeof frameId === 'string' ? this.#frames.get(frameId) : undefined;
|
||||
const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
|
||||
let world: IsolatedWorld | undefined;
|
||||
if (frame) {
|
||||
// Only care about execution contexts created for the current session.
|
||||
|
@ -513,7 +442,7 @@ export class FrameManager extends EventEmitter {
|
|||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
frame._detach();
|
||||
this.#frames.delete(frame._id);
|
||||
this._frameTree.removeFrame(frame);
|
||||
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {source as injectedSource} from '../generated/injected.js';
|
||||
import type PuppeteerUtil from '../injected/injected.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {ElementHandle} from './ElementHandle.js';
|
||||
import {TimeoutError} from './Errors.js';
|
||||
import {ExecutionContext} from './ExecutionContext.js';
|
||||
import {Frame} from './Frame.js';
|
||||
import {FrameManager} from './FrameManager.js';
|
||||
import {MouseButton} from './Input.js';
|
||||
import {JSHandle} from './JSHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
||||
import {TimeoutSettings} from './TimeoutSettings.js';
|
||||
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
||||
import {
|
||||
createJSHandle,
|
||||
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;
|
||||
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
|
||||
import {TaskManager, WaitTask} from './WaitTask.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -114,7 +98,6 @@ export interface IsolatedWorldChart {
|
|||
*/
|
||||
export class IsolatedWorld {
|
||||
#frame: Frame;
|
||||
#injected: boolean;
|
||||
#document?: ElementHandle<Document>;
|
||||
#context = createDeferredPromise<ExecutionContext>();
|
||||
#detached = false;
|
||||
|
@ -124,10 +107,15 @@ export class IsolatedWorld {
|
|||
|
||||
// Contains mapping from functions that should be bound to Puppeteer functions.
|
||||
#boundFunctions = new Map<string, Function>();
|
||||
#waitTasks = new Set<WaitTask>();
|
||||
#taskManager = new TaskManager();
|
||||
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
|
||||
|
||||
get _waitTasks(): Set<WaitTask> {
|
||||
return this.#waitTasks;
|
||||
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
|
||||
return this.#puppeteerUtil;
|
||||
}
|
||||
|
||||
get taskManager(): TaskManager {
|
||||
return this.#taskManager;
|
||||
}
|
||||
|
||||
get _boundFunctions(): Map<string, Function> {
|
||||
|
@ -138,11 +126,10 @@ export class IsolatedWorld {
|
|||
return `${name}_${contextId}`;
|
||||
};
|
||||
|
||||
constructor(frame: Frame, injected = false) {
|
||||
constructor(frame: Frame) {
|
||||
// Keep own reference to client because it might differ from the FrameManager's
|
||||
// client for OOP iframes.
|
||||
this.#frame = frame;
|
||||
this.#injected = injected;
|
||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
}
|
||||
|
||||
|
@ -164,17 +151,30 @@ export class IsolatedWorld {
|
|||
|
||||
clearContext(): void {
|
||||
this.#document = undefined;
|
||||
this.#puppeteerUtil = createDeferredPromise();
|
||||
this.#context = createDeferredPromise();
|
||||
}
|
||||
|
||||
setContext(context: ExecutionContext): void {
|
||||
if (this.#injected) {
|
||||
context.evaluate(injectedSource).catch(debugError);
|
||||
}
|
||||
this.#injectPuppeteerUtil(context);
|
||||
this.#ctxBindings.clear();
|
||||
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 {
|
||||
this.#detached = true;
|
||||
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
for (const waitTask of this._waitTasks) {
|
||||
waitTask.terminate(
|
||||
new Error('waitForFunction failed: frame got detached.')
|
||||
);
|
||||
}
|
||||
this.#taskManager.terminateAll(
|
||||
new Error('waitForFunction failed: frame got detached.')
|
||||
);
|
||||
}
|
||||
|
||||
executionContext(): Promise<ExecutionContext> {
|
||||
|
@ -411,8 +409,6 @@ export class IsolatedWorld {
|
|||
// TODO: In theory, it would be enough to call this just once
|
||||
await context._client.send('Runtime.addBinding', {
|
||||
name,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore The protocol definition is not up to date.
|
||||
executionContextName: context._contextName,
|
||||
});
|
||||
await context.evaluate(expression);
|
||||
|
@ -420,18 +416,19 @@ export class IsolatedWorld {
|
|||
// We could have tried to evaluate in a context which was already
|
||||
// destroyed. This happens, for example, if the page is navigated while
|
||||
// we are trying to add the binding
|
||||
const ctxDestroyed = (error as Error).message.includes(
|
||||
'Execution context was destroyed'
|
||||
);
|
||||
const ctxNotFound = (error as Error).message.includes(
|
||||
'Cannot find context with specified id'
|
||||
);
|
||||
if (ctxDestroyed || ctxNotFound) {
|
||||
return;
|
||||
} else {
|
||||
debugError(error);
|
||||
return;
|
||||
if (error instanceof Error) {
|
||||
// Destroyed context.
|
||||
if (error.message.includes('Execution context was destroyed')) {
|
||||
return;
|
||||
}
|
||||
// Missing context.
|
||||
if (error.message.includes('Cannot find context with specified id')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
debugError(error);
|
||||
return;
|
||||
}
|
||||
this.#ctxBindings.add(
|
||||
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
||||
|
@ -476,7 +473,17 @@ export class IsolatedWorld {
|
|||
throw new Error(`Bound function $name is not found`);
|
||||
}
|
||||
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) {
|
||||
// The WaitTask may already have been resolved by timing out, or the
|
||||
// exection context may have been destroyed.
|
||||
|
@ -488,14 +495,6 @@ export class IsolatedWorld {
|
|||
}
|
||||
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(
|
||||
|
@ -503,59 +502,97 @@ export class IsolatedWorld {
|
|||
root: ElementHandle<Node> | undefined,
|
||||
selector: string,
|
||||
options: WaitForSelectorOptions,
|
||||
binding?: PageBinding
|
||||
bindings = new Set<(...args: never[]) => unknown>()
|
||||
): Promise<JSHandle<unknown> | null> {
|
||||
const {
|
||||
visible: waitForVisible = false,
|
||||
hidden: waitForHidden = false,
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
} = options;
|
||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||
const title = `selector \`${selector}\`${
|
||||
waitForHidden ? ' to be hidden' : ''
|
||||
}`;
|
||||
async function predicate(
|
||||
root: Element | Document,
|
||||
selector: string,
|
||||
waitForVisible: boolean,
|
||||
waitForHidden: boolean
|
||||
): Promise<Node | null | boolean> {
|
||||
const node = (await predicateQueryHandler(root, selector)) as Element;
|
||||
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
||||
|
||||
try {
|
||||
const handle = await this.waitForFunction(
|
||||
async (PuppeteerUtil, query, selector, root, visible) => {
|
||||
if (!PuppeteerUtil) {
|
||||
return;
|
||||
}
|
||||
const node = (await PuppeteerUtil.createFunction(query)(
|
||||
root || document,
|
||||
selector,
|
||||
PuppeteerUtil
|
||||
)) 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(
|
||||
pageFunction: Function | string,
|
||||
options: {polling?: string | number; timeout?: number} = {},
|
||||
...args: unknown[]
|
||||
): Promise<JSHandle> {
|
||||
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
|
||||
options;
|
||||
const waitTaskOptions: WaitTaskOptions = {
|
||||
isolatedWorld: this,
|
||||
predicateBody: pageFunction,
|
||||
predicateAcceptsContextElement: false,
|
||||
title: 'function',
|
||||
polling,
|
||||
timeout,
|
||||
args,
|
||||
};
|
||||
const waitTask = new WaitTask(waitTaskOptions);
|
||||
return waitTask.promise;
|
||||
waitForFunction<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
options: {
|
||||
polling?: 'raf' | 'mutation' | number;
|
||||
timeout?: number;
|
||||
root?: ElementHandle<Node>;
|
||||
bindings?: Set<(...args: never[]) => unknown>;
|
||||
} = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
bindings,
|
||||
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> {
|
||||
|
@ -593,315 +630,3 @@ export class IsolatedWorld {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {HTTPRequest} from './HTTPRequest.js';
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import {Protocol} from 'devtools-protocol';
|
||||
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {Frame} from './Frame.js';
|
||||
|
@ -25,6 +24,7 @@ import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
|
|||
import {debugError, isString} from './util.js';
|
||||
import {DeferredPromise} from '../util/DeferredPromise.js';
|
||||
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
|
||||
import {CDPSession} from './Connection.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -66,13 +66,6 @@ export const NetworkManagerEmittedEvents = {
|
|||
RequestFinished: Symbol('NetworkManager.RequestFinished'),
|
||||
} 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 {
|
||||
frame(frameId: string): Frame | null;
|
||||
}
|
||||
|
|
|
@ -23,8 +23,12 @@ import {
|
|||
} from '../util/DeferredPromise.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {Accessibility} from './Accessibility.js';
|
||||
import {Browser, BrowserContext} from './Browser.js';
|
||||
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
|
||||
import type {Browser, BrowserContext} from '../api/Browser.js';
|
||||
import {
|
||||
CDPSession,
|
||||
CDPSessionEmittedEvents,
|
||||
isTargetClosedError,
|
||||
} from './Connection.js';
|
||||
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
||||
import {Coverage} from './Coverage.js';
|
||||
import {Dialog} from './Dialog.js';
|
||||
|
@ -36,6 +40,7 @@ import {
|
|||
Frame,
|
||||
FrameAddScriptTagOptions,
|
||||
FrameAddStyleTagOptions,
|
||||
FrameWaitForFunctionOptions,
|
||||
} from './Frame.js';
|
||||
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
|
||||
import {HTTPRequest} from './HTTPRequest.js';
|
||||
|
@ -470,7 +475,15 @@ export class Page extends EventEmitter {
|
|||
);
|
||||
await page.#initialize();
|
||||
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;
|
||||
}
|
||||
|
@ -645,11 +658,19 @@ export class Page extends EventEmitter {
|
|||
};
|
||||
|
||||
async #initialize(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.#frameManager.initialize(this.#target._targetId),
|
||||
this.#client.send('Performance.enable'),
|
||||
this.#client.send('Log.enable'),
|
||||
]);
|
||||
try {
|
||||
await Promise.all([
|
||||
this.#frameManager.initialize(),
|
||||
this.#client.send('Performance.enable'),
|
||||
this.#client.send('Log.enable'),
|
||||
]);
|
||||
} catch (err) {
|
||||
if (isErrorLike(err) && isTargetClosedError(err)) {
|
||||
debugError(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #onFileChooser(
|
||||
|
@ -3544,32 +3565,14 @@ export class Page extends EventEmitter {
|
|||
* ```
|
||||
*
|
||||
* @param pageFunction - Function to be evaluated in browser context
|
||||
* @param options - Optional waiting parameters
|
||||
*
|
||||
* - `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.
|
||||
* @param options - Options for configuring waiting behavior.
|
||||
*/
|
||||
waitForFunction<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
options: {
|
||||
timeout?: number;
|
||||
polling?: string | number;
|
||||
} = {},
|
||||
options: FrameWaitForFunctionOptions = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
|
|
|
@ -13,8 +13,11 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {Browser} from './Browser.js';
|
||||
import {BrowserConnectOptions, _connectToBrowser} from './BrowserConnector.js';
|
||||
import {Browser} from '../api/Browser.js';
|
||||
import {
|
||||
BrowserConnectOptions,
|
||||
_connectToCDPBrowser,
|
||||
} from './BrowserConnector.js';
|
||||
import {ConnectionTransport} from './ConnectionTransport.js';
|
||||
import {devices} from './DeviceDescriptors.js';
|
||||
import {errors} from './Errors.js';
|
||||
|
@ -54,7 +57,13 @@ export interface ConnectOptions extends BrowserConnectOptions {
|
|||
* @public
|
||||
*/
|
||||
export class Puppeteer {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected _isPuppeteerCore: boolean;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected _changedProduct = false;
|
||||
|
||||
/**
|
||||
|
@ -75,7 +84,7 @@ export class Puppeteer {
|
|||
* @returns Promise which resolves to browser instance.
|
||||
*/
|
||||
connect(options: ConnectOptions): Promise<Browser> {
|
||||
return _connectToBrowser(options);
|
||||
return _connectToCDPBrowser(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import PuppeteerUtil from '../injected/injected.js';
|
||||
import {ariaHandler} from './AriaQueryHandler.js';
|
||||
import {ElementHandle} from './ElementHandle.js';
|
||||
import {Frame} from './Frame.js';
|
||||
|
@ -41,6 +42,28 @@ export interface CustomQueryHandler {
|
|||
* @internal
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
|
@ -71,15 +94,19 @@ export interface InternalQueryHandler {
|
|||
) => Promise<ElementHandle<Node> | null>;
|
||||
}
|
||||
|
||||
function internalizeCustomQueryHandler(
|
||||
handler: CustomQueryHandler
|
||||
): InternalQueryHandler {
|
||||
const internalHandler: InternalQueryHandler = {};
|
||||
function createPuppeteerQueryHandler(
|
||||
handler: InternalQueryHandler
|
||||
): PuppeteerQueryHandler {
|
||||
const internalHandler: PuppeteerQueryHandler = {};
|
||||
|
||||
if (handler.queryOne) {
|
||||
const queryOne = handler.queryOne;
|
||||
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();
|
||||
if (elementHandle) {
|
||||
return elementHandle;
|
||||
|
@ -121,7 +148,11 @@ function internalizeCustomQueryHandler(
|
|||
if (handler.queryAll) {
|
||||
const queryAll = handler.queryAll;
|
||||
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();
|
||||
await jsHandle.dispose();
|
||||
const result = [];
|
||||
|
@ -138,7 +169,7 @@ function internalizeCustomQueryHandler(
|
|||
return internalHandler;
|
||||
}
|
||||
|
||||
const defaultHandler = internalizeCustomQueryHandler({
|
||||
const defaultHandler = createPuppeteerQueryHandler({
|
||||
queryOne: (element, selector) => {
|
||||
if (!('querySelector' in element)) {
|
||||
throw new Error(
|
||||
|
@ -165,87 +196,35 @@ const defaultHandler = internalizeCustomQueryHandler({
|
|||
},
|
||||
});
|
||||
|
||||
const pierceHandler = internalizeCustomQueryHandler({
|
||||
queryOne: (element, selector) => {
|
||||
let found: Node | null = null;
|
||||
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;
|
||||
const pierceHandler = createPuppeteerQueryHandler({
|
||||
queryOne: (element, selector, {pierceQuerySelector}) => {
|
||||
return pierceQuerySelector(element, selector);
|
||||
},
|
||||
|
||||
queryAll: (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;
|
||||
queryAll: (element, selector, {pierceQuerySelectorAll}) => {
|
||||
return pierceQuerySelectorAll(element, selector);
|
||||
},
|
||||
});
|
||||
|
||||
const xpathHandler = internalizeCustomQueryHandler({
|
||||
queryOne: (element, selector) => {
|
||||
const doc = element.ownerDocument || document;
|
||||
const result = doc.evaluate(
|
||||
selector,
|
||||
element,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE
|
||||
);
|
||||
return result.singleNodeValue;
|
||||
const xpathHandler = createPuppeteerQueryHandler({
|
||||
queryOne: (element, selector, {xpathQuerySelector}) => {
|
||||
return xpathQuerySelector(element, selector);
|
||||
},
|
||||
queryAll: (element, selector, {xpathQuerySelectorAll}) => {
|
||||
return xpathQuerySelectorAll(element, selector);
|
||||
},
|
||||
});
|
||||
|
||||
queryAll: (element, selector) => {
|
||||
const doc = element.ownerDocument || document;
|
||||
const iterator = doc.evaluate(
|
||||
selector,
|
||||
element,
|
||||
null,
|
||||
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||
);
|
||||
const array: Node[] = [];
|
||||
let item;
|
||||
while ((item = iterator.iterateNext())) {
|
||||
array.push(item);
|
||||
}
|
||||
return array;
|
||||
const textQueryHandler = createPuppeteerQueryHandler({
|
||||
queryOne: (element, selector, {textQuerySelector}) => {
|
||||
return textQuerySelector(element, selector);
|
||||
},
|
||||
queryAll: (element, selector, {textQuerySelectorAll}) => {
|
||||
return textQuerySelectorAll(element, selector);
|
||||
},
|
||||
});
|
||||
|
||||
interface RegisteredQueryHandler {
|
||||
handler: InternalQueryHandler;
|
||||
handler: PuppeteerQueryHandler;
|
||||
transformSelector?: (selector: string) => string;
|
||||
}
|
||||
|
||||
|
@ -253,6 +232,7 @@ const INTERNAL_QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>([
|
|||
['aria', {handler: ariaHandler}],
|
||||
['pierce', {handler: pierceHandler}],
|
||||
['xpath', {handler: xpathHandler}],
|
||||
['text', {handler: textQueryHandler}],
|
||||
]);
|
||||
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]`);
|
||||
}
|
||||
|
||||
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): {
|
||||
updatedSelector: string;
|
||||
queryHandler: InternalQueryHandler;
|
||||
queryHandler: PuppeteerQueryHandler;
|
||||
} {
|
||||
for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) {
|
||||
for (const [
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
import {Page, PageEmittedEvents} from './Page.js';
|
||||
import {WebWorker} from './WebWorker.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 {Protocol} from 'devtools-protocol';
|
||||
import {TaskQueue} from './TaskQueue.js';
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 {ElementHandle} from './ElementHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
|
|||
* @public
|
||||
*/
|
||||
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export function pageBindingInitString(type: string, name: string): string {
|
||||
function addPageBinding(type: string, bindingName: string): void {
|
||||
/* Cast window to any here as we're about to add properties to it
|
||||
* via win[bindingName] which TypeScript doesn't like.
|
||||
*/
|
||||
const win = window as any;
|
||||
const binding = win[bindingName];
|
||||
function addPageBinding(type: string, name: string): void {
|
||||
// This is the CDP binding.
|
||||
// @ts-expect-error: In a different context.
|
||||
const callCDP = self[name];
|
||||
|
||||
win[bindingName] = (...args: unknown[]): Promise<unknown> => {
|
||||
const me = (window as any)[bindingName];
|
||||
let callbacks = me.callbacks;
|
||||
if (!callbacks) {
|
||||
callbacks = new Map();
|
||||
me.callbacks = callbacks;
|
||||
}
|
||||
const seq = (me.lastSeq || 0) + 1;
|
||||
me.lastSeq = seq;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
return callbacks.set(seq, {resolve, reject});
|
||||
});
|
||||
binding(JSON.stringify({type, name: bindingName, seq, args}));
|
||||
return promise;
|
||||
};
|
||||
// We replace the CDP binding with a Puppeteer binding.
|
||||
Object.assign(self, {
|
||||
[name](...args: unknown[]): Promise<unknown> {
|
||||
// This is the Puppeteer binding.
|
||||
// @ts-expect-error: In a different context.
|
||||
const callPuppeteer = self[name];
|
||||
callPuppeteer.callbacks ??= new Map();
|
||||
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
|
||||
callPuppeteer.lastSeq = seq;
|
||||
callCDP(JSON.stringify({type, name, seq, args}));
|
||||
return new Promise((resolve, reject) => {
|
||||
callPuppeteer.callbacks.set(seq, {resolve, reject});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return evaluationString(addPageBinding, type, name);
|
||||
}
|
||||
|
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
||||
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 {puppeteerDirname} from './compat.js';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const packageVersion = '17.1.2';
|
||||
export const packageVersion = '18.0.0';
|
||||
|
|
|
@ -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 {
|
||||
createDeferredPromise,
|
||||
DeferredPromise,
|
||||
} 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>;
|
||||
result(): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class MutationPoller<T> implements Poller<T> {
|
||||
#fn: () => Promise<T>;
|
||||
|
||||
|
@ -22,12 +44,12 @@ export class MutationPoller<T> implements Poller<T> {
|
|||
this.#root = root;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#observer = new MutationObserver(async () => {
|
||||
|
@ -43,8 +65,6 @@ export class MutationPoller<T> implements Poller<T> {
|
|||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
|
@ -54,6 +74,7 @@ export class MutationPoller<T> implements Poller<T> {
|
|||
}
|
||||
if (this.#observer) {
|
||||
this.#observer.disconnect();
|
||||
this.#observer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,12 +91,12 @@ export class RAFPoller<T> implements Poller<T> {
|
|||
this.#fn = fn;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
|
@ -91,8 +112,6 @@ export class RAFPoller<T> implements Poller<T> {
|
|||
await this.stop();
|
||||
};
|
||||
window.requestAnimationFrame(poll);
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
|
@ -119,12 +138,12 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||
this.#ms = ms;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#interval = setInterval(async () => {
|
||||
|
@ -135,8 +154,6 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||
promise.resolve(result);
|
||||
await this.stop();
|
||||
}, this.#ms);
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
|
@ -146,6 +163,7 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||
}
|
||||
if (this.#interval) {
|
||||
clearInterval(this.#interval);
|
||||
this.#interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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 [];
|
||||
};
|
|
@ -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 * 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';
|
||||
|
||||
Object.assign(
|
||||
self,
|
||||
Object.freeze({
|
||||
InjectedUtil: {
|
||||
...Poller,
|
||||
...util,
|
||||
createDeferredPromise,
|
||||
},
|
||||
})
|
||||
);
|
||||
const PuppeteerUtil = Object.freeze({
|
||||
...util,
|
||||
...Poller,
|
||||
...TextContent,
|
||||
...TextQuerySelector,
|
||||
...XPathQuerySelector,
|
||||
...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>();
|
||||
|
||||
/**
|
||||
* Creates a function from a string.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const createFunction = (
|
||||
functionValue: string
|
||||
|
@ -16,3 +34,42 @@ export const createFunction = (
|
|||
createdFunctions.set(functionValue, 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';
|
||||
break;
|
||||
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;
|
||||
default:
|
||||
assert(false, 'Unsupported platform: ' + platform);
|
||||
|
@ -336,7 +341,7 @@ export class BrowserFetcher {
|
|||
}
|
||||
|
||||
// Use system Chromium builds on Linux ARM devices
|
||||
if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
|
||||
if (os.platform() === 'linux' && os.arch() === 'arm64') {
|
||||
handleArm64();
|
||||
return;
|
||||
}
|
||||
|
@ -497,6 +502,25 @@ function parseFolderPath(
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,7 @@ import removeFolder from 'rimraf';
|
|||
import {promisify} from 'util';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Connection} from '../common/Connection.js';
|
||||
import {Connection as BiDiConnection} from '../common/bidi/Connection.js';
|
||||
import {debug} from '../common/Debug.js';
|
||||
import {TimeoutError} from '../common/Errors.js';
|
||||
import {
|
||||
|
@ -245,6 +246,25 @@ export class BrowserRunner {
|
|||
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: {
|
||||
usePipe?: boolean;
|
||||
timeout: number;
|
||||
|
@ -279,7 +299,8 @@ export class BrowserRunner {
|
|||
function waitForWSEndpoint(
|
||||
browserProcess: childProcess.ChildProcess,
|
||||
timeout: number,
|
||||
preferredRevision: string
|
||||
preferredRevision: string,
|
||||
regex = /^DevTools listening on (ws:\/\/.*)$/
|
||||
): Promise<string> {
|
||||
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
|
||||
const rl = readline.createInterface(browserProcess.stderr);
|
||||
|
@ -327,7 +348,7 @@ function waitForWSEndpoint(
|
|||
|
||||
function onLine(line: string): void {
|
||||
stderr += line + '\n';
|
||||
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
|
||||
const match = line.match(regex);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
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 {BrowserRunner} from './BrowserRunner.js';
|
||||
import {
|
||||
|
@ -43,7 +43,7 @@ export class ChromeLauncher implements ProductLauncher {
|
|||
this._isPuppeteerCore = isPuppeteerCore;
|
||||
}
|
||||
|
||||
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
|
||||
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<CDPBrowser> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
|
@ -154,7 +154,7 @@ export class ChromeLauncher implements ProductLauncher {
|
|||
slowMo,
|
||||
preferredRevision: this._preferredRevision,
|
||||
});
|
||||
browser = await Browser._create(
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
connection,
|
||||
[],
|
||||
|
|
|
@ -2,7 +2,9 @@ import fs from 'fs';
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
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 {BrowserFetcher} from './BrowserFetcher.js';
|
||||
import {BrowserRunner} from './BrowserRunner.js';
|
||||
|
@ -58,6 +60,7 @@ export class FirefoxLauncher implements ProductLauncher {
|
|||
extraPrefsFirefox = {},
|
||||
waitForInitialPage = true,
|
||||
debuggingPort = null,
|
||||
protocol = 'cdp',
|
||||
} = options;
|
||||
|
||||
const firefoxArguments = [];
|
||||
|
@ -113,7 +116,9 @@ export class FirefoxLauncher implements ProductLauncher {
|
|||
firefoxArguments.push(userDataDir);
|
||||
}
|
||||
|
||||
await this._updateRevision();
|
||||
if (!this._isPuppeteerCore) {
|
||||
await this._updateRevision();
|
||||
}
|
||||
let firefoxExecutable = executablePath;
|
||||
if (!executablePath) {
|
||||
const {missingText, executablePath} = resolveExecutablePath(this);
|
||||
|
@ -143,6 +148,27 @@ export class FirefoxLauncher implements ProductLauncher {
|
|||
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;
|
||||
try {
|
||||
const connection = await runner.setupConnection({
|
||||
|
@ -151,7 +177,7 @@ export class FirefoxLauncher implements ProductLauncher {
|
|||
slowMo,
|
||||
preferredRevision: this._preferredRevision,
|
||||
});
|
||||
browser = await Browser._create(
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
connection,
|
||||
[],
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
import os from 'os';
|
||||
|
||||
import {Browser} from '../common/Browser.js';
|
||||
import {Browser} from '../api/Browser.js';
|
||||
import {BrowserFetcher} from './BrowserFetcher.js';
|
||||
|
||||
import {
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
|
||||
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.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 {PUPPETEER_REVISIONS} from '../revisions.js';
|
||||
import {Product} from '../common/Product.js';
|
||||
|
@ -78,6 +78,9 @@ export class PuppeteerNode extends Puppeteer {
|
|||
#projectRoot?: string;
|
||||
#productName?: Product;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_preferredRevision: string;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||
|
||||
declare global {
|
||||
const InjectedUtil: {
|
||||
createDeferredPromise: typeof createDeferredPromise;
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
/**
|
||||
* CommonJS JavaScript code that provides the puppeteer utilities. See the
|
||||
* [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md)
|
||||
* for injection for more information.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const source = SOURCE_CODE;
|
||||
|
|
|
@ -8,6 +8,5 @@
|
|||
"references": [
|
||||
{"path": "../vendor/tsconfig.cjs.json"},
|
||||
{"path": "../compat/cjs/tsconfig.json"}
|
||||
],
|
||||
"exclude": ["injected/injected.ts"]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,6 +8,5 @@
|
|||
"references": [
|
||||
{"path": "../vendor/tsconfig.esm.json"},
|
||||
{"path": "../compat/esm/tsconfig.json"}
|
||||
],
|
||||
"exclude": ["injected/injected.ts"]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// AUTOGENERATED - Use `npm run generate:sources` to regenerate.
|
||||
|
||||
export * from './api/Browser.js';
|
||||
export * from './common/Accessibility.js';
|
||||
export * from './common/AriaQueryHandler.js';
|
||||
export * from './common/Browser.js';
|
||||
|
@ -23,11 +24,13 @@ export * from './common/FileChooser.js';
|
|||
export * from './common/FirefoxTargetManager.js';
|
||||
export * from './common/Frame.js';
|
||||
export * from './common/FrameManager.js';
|
||||
export * from './common/FrameTree.js';
|
||||
export * from './common/HTTPRequest.js';
|
||||
export * from './common/HTTPResponse.js';
|
||||
export * from './common/Input.js';
|
||||
export * from './common/IsolatedWorld.js';
|
||||
export * from './common/JSHandle.js';
|
||||
export * from './common/LazyArg.js';
|
||||
export * from './common/LifecycleWatcher.js';
|
||||
export * from './common/NetworkConditions.js';
|
||||
export * from './common/NetworkEventManager.js';
|
||||
|
@ -47,12 +50,12 @@ export * from './common/Tracing.js';
|
|||
export * from './common/types.js';
|
||||
export * from './common/USKeyboardLayout.js';
|
||||
export * from './common/util.js';
|
||||
export * from './common/WaitTask.js';
|
||||
export * from './common/WebWorker.js';
|
||||
export * from './compat.d.js';
|
||||
export * from './constants.js';
|
||||
export * from './environment.js';
|
||||
export * from './generated/injected.js';
|
||||
export * from './generated/version.js';
|
||||
export * from './initializePuppeteer.js';
|
||||
export * from './node/BrowserFetcher.js';
|
||||
export * from './node/BrowserRunner.js';
|
||||
|
|
|
@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
|
|||
export interface DeferredPromise<T> extends Promise<T> {
|
||||
finished: () => boolean;
|
||||
resolved: () => boolean;
|
||||
resolve: (_: T) => void;
|
||||
reject: (_: Error) => void;
|
||||
resolve: (value: T) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
|
|||
): DeferredPromise<T> {
|
||||
let isResolved = false;
|
||||
let isRejected = false;
|
||||
let resolver = (_: T): void => {};
|
||||
let rejector = (_: Error) => {};
|
||||
let resolver: (value: T) => void;
|
||||
let rejector: (reason?: unknown) => void;
|
||||
const taskPromise = new Promise<T>((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
|
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
|
|||
isResolved = true;
|
||||
resolver(value);
|
||||
},
|
||||
reject: (err: Error) => {
|
||||
reject: (err?: unknown) => {
|
||||
clearTimeout(timeoutId);
|
||||
isRejected = true;
|
||||
rejector(err);
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 8.3 KiB |
|
@ -20,11 +20,10 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||
|
||||
describeChromeOnly('Target.createCDPSession', function () {
|
||||
describe('Target.createCDPSession', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {describeChromeOnly} from './mocha-utils.js';
|
||||
|
||||
import expect from 'expect';
|
||||
import {
|
||||
NetworkManager,
|
||||
|
@ -28,9 +26,16 @@ import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
|||
|
||||
class MockCDPSession extends EventEmitter {
|
||||
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 () => {
|
||||
const mockCDPSession = new MockCDPSession();
|
||||
new NetworkManager(mockCDPSession, true, {
|
||||
|
|
|
@ -14,24 +14,24 @@
|
|||
* 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 expect from 'expect';
|
||||
|
||||
import {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
CDPBrowser,
|
||||
CDPBrowserContext,
|
||||
} 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 */
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let browser: CDPBrowser;
|
||||
let context: CDPBrowserContext;
|
||||
|
||||
before(async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
browser = await puppeteer.launch(
|
||||
browser = (await puppeteer.launch(
|
||||
Object.assign({}, defaultBrowserOptions, {
|
||||
args: (defaultBrowserOptions.args || []).concat([
|
||||
'--site-per-process',
|
||||
|
@ -39,7 +39,7 @@ describeChromeOnly('TargetManager', () => {
|
|||
'--host-rules=MAP * 127.0.0.1',
|
||||
]),
|
||||
})
|
||||
);
|
||||
)) as CDPBrowser;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
@ -21,10 +21,9 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
describeFailsFirefox,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describeFailsFirefox('Accessibility', function () {
|
||||
describe('Accessibility', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
|
@ -346,7 +345,7 @@ describeFailsFirefox('Accessibility', function () {
|
|||
});
|
||||
|
||||
// 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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
|
|
@ -19,14 +19,13 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js';
|
||||
import utils from './utils.js';
|
||||
import assert from 'assert';
|
||||
|
||||
describeChromeOnly('AriaQueryHandler', () => {
|
||||
describe('AriaQueryHandler', () => {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
|
@ -447,7 +446,7 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
|
||||
let divHidden = false;
|
||||
await page.setContent(
|
||||
`<div role='button' style='display: block;'></div>`
|
||||
`<div role='button' style='display: block;'>text</div>`
|
||||
);
|
||||
const waitForSelector = page
|
||||
.waitForSelector('aria/[role="button"]', {hidden: true})
|
||||
|
@ -469,7 +468,9 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
const {page} = getTestState();
|
||||
|
||||
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
|
||||
.waitForSelector('aria/[role="main"]', {hidden: true})
|
||||
.then(() => {
|
||||
|
@ -489,7 +490,7 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
it('hidden should wait for removal', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
await page.setContent(`<div role='main'></div>`);
|
||||
await page.setContent(`<div role='main'>text</div>`);
|
||||
let divRemoved = false;
|
||||
const waitForSelector = page
|
||||
.waitForSelector('aria/[role="main"]', {hidden: true})
|
||||
|
@ -517,15 +518,15 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
it('should respect timeout', async () => {
|
||||
const {page, puppeteer} = getTestState();
|
||||
|
||||
let error!: Error;
|
||||
await page
|
||||
.waitForSelector('aria/[role="button"]', {timeout: 10})
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
const error = await page
|
||||
.waitForSelector('aria/[role="button"]', {
|
||||
timeout: 10,
|
||||
})
|
||||
.catch(error => {
|
||||
return error;
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
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);
|
||||
});
|
||||
|
@ -533,17 +534,15 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
await page.setContent(`<div role='main'></div>`);
|
||||
let error!: Error;
|
||||
await page
|
||||
.waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10})
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain(
|
||||
'waiting for selector `[role="main"]` to be hidden failed: timeout'
|
||||
);
|
||||
await page.setContent(`<div role='main'>text</div>`);
|
||||
const promise = page.waitForSelector('aria/[role="main"]', {
|
||||
hidden: true,
|
||||
timeout: 10,
|
||||
});
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
message:
|
||||
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond to node attribute mutation', async () => {
|
||||
|
@ -582,7 +581,9 @@ describeChromeOnly('AriaQueryHandler', () => {
|
|||
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
} from './mocha-utils.js';
|
||||
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||
import {waitEvent} from './utils.js';
|
||||
|
||||
describe('BrowserContext', function () {
|
||||
|
|
|
@ -19,10 +19,9 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describeChromeOnly('Chromium-Specific Launcher tests', function () {
|
||||
describe('Chromium-Specific Launcher tests', function () {
|
||||
describe('Puppeteer.launch |browserURL| option', function () {
|
||||
it('should be able to connect using browserUrl, with and without trailing slash', async () => {
|
||||
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();
|
||||
setupTestPageAndContextHooks();
|
||||
it('Page.setRequestInterception should work with intervention headers', async () => {
|
||||
|
|
|
@ -51,24 +51,21 @@ describe('Page.click', function () {
|
|||
})
|
||||
).toBe(42);
|
||||
});
|
||||
it(
|
||||
'should click the button if window.Node is removed',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should click the button if window.Node is removed', 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(() => {
|
||||
// @ts-expect-error Expected.
|
||||
return delete window.Node;
|
||||
});
|
||||
await page.click('button');
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).result;
|
||||
})
|
||||
).toBe('Clicked');
|
||||
}
|
||||
);
|
||||
return (globalThis as any).result;
|
||||
})
|
||||
).toBe('Clicked');
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4281
|
||||
it('should click on a span with an inline element inside', async () => {
|
||||
const {page} = getTestState();
|
||||
|
@ -421,7 +418,7 @@ describe('Page.click', function () {
|
|||
).toBe('Clicked');
|
||||
});
|
||||
// @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();
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
it(
|
||||
'should default to setting secure cookie for HTTPS websites',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should default to setting secure cookie for HTTPS websites', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const SECURE_URL = 'https://example.com';
|
||||
await page.setCookie({
|
||||
url: SECURE_URL,
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
});
|
||||
const [cookie] = await page.cookies(SECURE_URL);
|
||||
expect(cookie!.secure).toBe(true);
|
||||
}
|
||||
);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const SECURE_URL = 'https://example.com';
|
||||
await page.setCookie({
|
||||
url: SECURE_URL,
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
});
|
||||
const [cookie] = await page.cookies(SECURE_URL);
|
||||
expect(cookie!.secure).toBe(true);
|
||||
});
|
||||
it('should be able to set unsecure cookie for HTTP website', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
@ -481,67 +478,64 @@ describe('Cookie specs', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
it(
|
||||
'should set secure same-site cookies from a frame',
|
||||
async () => {
|
||||
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
it('should set secure same-site cookies from a frame', async () => {
|
||||
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
...defaultBrowserOptions,
|
||||
ignoreHTTPSErrors: true,
|
||||
const browser = await puppeteer.launch({
|
||||
...defaultBrowserOptions,
|
||||
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();
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
|
||||
'127-same-site-cookie=best'
|
||||
);
|
||||
expectCookieEquals(
|
||||
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();
|
||||
}
|
||||
expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
|
||||
'127-same-site-cookie=best'
|
||||
);
|
||||
expectCookieEquals(
|
||||
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 () {
|
||||
|
|
|
@ -19,11 +19,10 @@ import {
|
|||
getTestState,
|
||||
setupTestPageAndContextHooks,
|
||||
setupTestBrowserHooks,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describe('Coverage specs', function () {
|
||||
describeChromeOnly('JSCoverage', function () {
|
||||
describe('JSCoverage', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
|
@ -134,7 +133,7 @@ describe('Coverage specs', function () {
|
|||
).toBeGolden('jscoverage-involved.txt');
|
||||
});
|
||||
// @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();
|
||||
|
||||
await page.coverage.startJSCoverage();
|
||||
|
@ -190,7 +189,7 @@ describe('Coverage specs', function () {
|
|||
});
|
||||
});
|
||||
// @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();
|
||||
|
||||
await page.coverage.startJSCoverage();
|
||||
|
@ -202,7 +201,7 @@ describe('Coverage specs', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describeChromeOnly('CSSCoverage', function () {
|
||||
describe('CSSCoverage', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
|
|
|
@ -19,10 +19,9 @@ import {
|
|||
getTestState,
|
||||
setupTestPageAndContextHooks,
|
||||
setupTestBrowserHooks,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describeChromeOnly('Input.drag', function () {
|
||||
describe('Input.drag', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
it('should throw an exception if not enabled before usage', async () => {
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Evaluation specs', function () {
|
|||
});
|
||||
expect(result).toBe(21);
|
||||
});
|
||||
(bigint ? it : xit)('should transfer BigInt', async () => {
|
||||
(bigint ? it : it.skip)('should transfer BigInt', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const result = await page.evaluate((a: bigint) => {
|
||||
|
@ -113,18 +113,15 @@ describe('Evaluation specs', function () {
|
|||
await page.goto(server.PREFIX + '/global-var.html');
|
||||
expect(await page.evaluate('globalVar')).toBe(123);
|
||||
});
|
||||
it(
|
||||
'should return undefined for objects with symbols',
|
||||
async () => {
|
||||
const {page} = getTestState();
|
||||
it('should return undefined for objects with symbols', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return [Symbol('foo4')];
|
||||
})
|
||||
).toBe(undefined);
|
||||
}
|
||||
);
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return [Symbol('foo4')];
|
||||
})
|
||||
).toBe(undefined);
|
||||
});
|
||||
it('should work with function shorthands', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
@ -261,7 +258,7 @@ describe('Evaluation specs', function () {
|
|||
expect(result).not.toBe(object);
|
||||
expect(result).toEqual(object);
|
||||
});
|
||||
(bigint ? it : xit)('should return BigInt', async () => {
|
||||
(bigint ? it : it.skip)('should return BigInt', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
|
@ -322,18 +319,15 @@ describe('Evaluation specs', function () {
|
|||
})
|
||||
).toEqual({});
|
||||
});
|
||||
it(
|
||||
'should return undefined for non-serializable objects',
|
||||
async () => {
|
||||
const {page} = getTestState();
|
||||
it('should return undefined for non-serializable objects', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return window;
|
||||
})
|
||||
).toBe(undefined);
|
||||
}
|
||||
);
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return window;
|
||||
})
|
||||
).toBe(undefined);
|
||||
});
|
||||
it('should fail for circular object', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
@ -408,27 +402,24 @@ describe('Evaluation specs', function () {
|
|||
});
|
||||
expect(error.message).toContain('JSHandle is disposed');
|
||||
});
|
||||
it(
|
||||
'should throw if elementHandles are from other frames',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should throw if elementHandles are from other frames', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const bodyHandle = await page.frames()[1]!.$('body');
|
||||
let error!: Error;
|
||||
await page
|
||||
.evaluate(body => {
|
||||
return body?.innerHTML;
|
||||
}, bodyHandle)
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain(
|
||||
'JSHandles can be evaluated only in the context they were created'
|
||||
);
|
||||
}
|
||||
);
|
||||
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const bodyHandle = await page.frames()[1]!.$('body');
|
||||
let error!: Error;
|
||||
await page
|
||||
.evaluate(body => {
|
||||
return body?.innerHTML;
|
||||
}, bodyHandle)
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain(
|
||||
'JSHandles can be evaluated only in the context they were created'
|
||||
);
|
||||
});
|
||||
it('should simulate a user gesture', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
@ -459,19 +450,16 @@ describe('Evaluation specs', function () {
|
|||
});
|
||||
expect((error as Error).message).toContain('navigation');
|
||||
});
|
||||
it(
|
||||
'should not throw an error when evaluation does a navigation',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should not throw an error when evaluation does a navigation', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
const result = await page.evaluate(() => {
|
||||
(window as any).location = '/empty.html';
|
||||
return [42];
|
||||
});
|
||||
expect(result).toEqual([42]);
|
||||
}
|
||||
);
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
const result = await page.evaluate(() => {
|
||||
(window as any).location = '/empty.html';
|
||||
return [42];
|
||||
});
|
||||
expect(result).toEqual([42]);
|
||||
});
|
||||
it('should transfer 100Mb of data from page to node.js', async function () {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
|
|
@ -17,12 +17,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
import expect from 'expect';
|
||||
import {getTestState, itHeadlessOnly} from './mocha-utils.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
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();
|
||||
if (headless === 'chrome') {
|
||||
// This test only works in the old headless mode.
|
||||
|
|
|
@ -137,40 +137,37 @@ describe('Frame specs', function () {
|
|||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||
]);
|
||||
});
|
||||
it(
|
||||
'should send events when frames are manipulated dynamically',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should send events when frames are manipulated dynamically', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
// validate frameattached events
|
||||
const attachedFrames: Frame[] = [];
|
||||
page.on('frameattached', frame => {
|
||||
return attachedFrames.push(frame);
|
||||
});
|
||||
await utils.attachFrame(page, 'frame1', './assets/frame.html');
|
||||
expect(attachedFrames.length).toBe(1);
|
||||
expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
// validate frameattached events
|
||||
const attachedFrames: Frame[] = [];
|
||||
page.on('frameattached', frame => {
|
||||
return attachedFrames.push(frame);
|
||||
});
|
||||
await utils.attachFrame(page, 'frame1', './assets/frame.html');
|
||||
expect(attachedFrames.length).toBe(1);
|
||||
expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
|
||||
|
||||
// validate framenavigated events
|
||||
const navigatedFrames: Frame[] = [];
|
||||
page.on('framenavigated', frame => {
|
||||
return navigatedFrames.push(frame);
|
||||
});
|
||||
await utils.navigateFrame(page, 'frame1', './empty.html');
|
||||
expect(navigatedFrames.length).toBe(1);
|
||||
expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||
// validate framenavigated events
|
||||
const navigatedFrames: Frame[] = [];
|
||||
page.on('framenavigated', frame => {
|
||||
return navigatedFrames.push(frame);
|
||||
});
|
||||
await utils.navigateFrame(page, 'frame1', './empty.html');
|
||||
expect(navigatedFrames.length).toBe(1);
|
||||
expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||
|
||||
// validate framedetached events
|
||||
const detachedFrames: Frame[] = [];
|
||||
page.on('framedetached', frame => {
|
||||
return detachedFrames.push(frame);
|
||||
});
|
||||
await utils.detachFrame(page, 'frame1');
|
||||
expect(detachedFrames.length).toBe(1);
|
||||
expect(detachedFrames[0]!.isDetached()).toBe(true);
|
||||
}
|
||||
);
|
||||
// validate framedetached events
|
||||
const detachedFrames: Frame[] = [];
|
||||
page.on('framedetached', frame => {
|
||||
return detachedFrames.push(frame);
|
||||
});
|
||||
await utils.detachFrame(page, 'frame1');
|
||||
expect(detachedFrames.length).toBe(1);
|
||||
expect(detachedFrames[0]!.isDetached()).toBe(true);
|
||||
});
|
||||
it('should send "framenavigated" when navigating on anchor URLs', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
@ -299,31 +296,24 @@ describe('Frame specs', function () {
|
|||
expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
|
||||
expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
|
||||
});
|
||||
it(
|
||||
'should report different frame instance when frame re-attaches',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should report different frame instance when frame re-attaches', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
const frame1 = await utils.attachFrame(
|
||||
page,
|
||||
'frame1',
|
||||
server.EMPTY_PAGE
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
(globalThis as any).frame = document.querySelector('#frame1');
|
||||
(globalThis as any).frame.remove();
|
||||
});
|
||||
expect(frame1!.isDetached()).toBe(true);
|
||||
const [frame2] = await Promise.all([
|
||||
utils.waitEvent(page, 'frameattached'),
|
||||
page.evaluate(() => {
|
||||
return document.body.appendChild((globalThis as any).frame);
|
||||
}),
|
||||
]);
|
||||
expect(frame2.isDetached()).toBe(false);
|
||||
expect(frame1).not.toBe(frame2);
|
||||
}
|
||||
);
|
||||
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
await page.evaluate(() => {
|
||||
(globalThis as any).frame = document.querySelector('#frame1');
|
||||
(globalThis as any).frame.remove();
|
||||
});
|
||||
expect(frame1!.isDetached()).toBe(true);
|
||||
const [frame2] = await Promise.all([
|
||||
utils.waitEvent(page, 'frameattached'),
|
||||
page.evaluate(() => {
|
||||
return document.body.appendChild((globalThis as any).frame);
|
||||
}),
|
||||
]);
|
||||
expect(frame2.isDetached()).toBe(false);
|
||||
expect(frame1).not.toBe(frame2);
|
||||
});
|
||||
it('should support url fragment', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
|
|
@ -24,11 +24,7 @@ import {
|
|||
PuppeteerLaunchOptions,
|
||||
PuppeteerNode,
|
||||
} from '../../lib/cjs/puppeteer/node/Puppeteer.js';
|
||||
import {
|
||||
describeChromeOnly,
|
||||
getTestState,
|
||||
itFailsWindows,
|
||||
} from './mocha-utils.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
|
||||
const rmAsync = promisify(rimraf);
|
||||
const mkdtempAsync = promisify(fs.mkdtemp);
|
||||
|
@ -44,7 +40,7 @@ const serviceWorkerExtensionPath = path.join(
|
|||
'extension'
|
||||
);
|
||||
|
||||
describeChromeOnly('headful tests', function () {
|
||||
describe('headful tests', function () {
|
||||
/* These tests fire up an actual browser so let's
|
||||
* allow a higher timeout
|
||||
*/
|
||||
|
@ -214,43 +210,40 @@ describeChromeOnly('headful tests', function () {
|
|||
expect(pages).toEqual(['about:blank']);
|
||||
await browser.close();
|
||||
});
|
||||
itFailsWindows(
|
||||
'headless should be able to read cookies written by headful',
|
||||
async () => {
|
||||
/* Needs investigation into why but this fails consistently on Windows CI. */
|
||||
const {server, puppeteer} = getTestState();
|
||||
it('headless should be able to read cookies written by headful', async () => {
|
||||
/* Needs investigation into why but this fails consistently on Windows CI. */
|
||||
const {server, puppeteer} = getTestState();
|
||||
|
||||
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||
// Write a cookie in headful chrome
|
||||
const headfulBrowser = await launchBrowser(
|
||||
puppeteer,
|
||||
Object.assign({userDataDir}, headfulOptions)
|
||||
);
|
||||
const headfulPage = await headfulBrowser.newPage();
|
||||
await headfulPage.goto(server.EMPTY_PAGE);
|
||||
await headfulPage.evaluate(() => {
|
||||
return (document.cookie =
|
||||
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
|
||||
});
|
||||
await headfulBrowser.close();
|
||||
// Read the cookie from headless chrome
|
||||
const headlessBrowser = await launchBrowser(
|
||||
puppeteer,
|
||||
Object.assign({userDataDir}, headlessOptions)
|
||||
);
|
||||
const headlessPage = await headlessBrowser.newPage();
|
||||
await headlessPage.goto(server.EMPTY_PAGE);
|
||||
const cookie = await headlessPage.evaluate(() => {
|
||||
return document.cookie;
|
||||
});
|
||||
await headlessBrowser.close();
|
||||
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
||||
await rmAsync(userDataDir).catch(() => {});
|
||||
expect(cookie).toBe('foo=true');
|
||||
}
|
||||
);
|
||||
const userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||
// Write a cookie in headful chrome
|
||||
const headfulBrowser = await launchBrowser(
|
||||
puppeteer,
|
||||
Object.assign({userDataDir}, headfulOptions)
|
||||
);
|
||||
const headfulPage = await headfulBrowser.newPage();
|
||||
await headfulPage.goto(server.EMPTY_PAGE);
|
||||
await headfulPage.evaluate(() => {
|
||||
return (document.cookie =
|
||||
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
|
||||
});
|
||||
await headfulBrowser.close();
|
||||
// Read the cookie from headless chrome
|
||||
const headlessBrowser = await launchBrowser(
|
||||
puppeteer,
|
||||
Object.assign({userDataDir}, headlessOptions)
|
||||
);
|
||||
const headlessPage = await headlessBrowser.newPage();
|
||||
await headlessPage.goto(server.EMPTY_PAGE);
|
||||
const cookie = await headlessPage.evaluate(() => {
|
||||
return document.cookie;
|
||||
});
|
||||
await headlessBrowser.close();
|
||||
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
|
||||
await rmAsync(userDataDir).catch(() => {});
|
||||
expect(cookie).toBe('foo=true');
|
||||
});
|
||||
// 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();
|
||||
|
||||
// https://google.com is isolated by default in Chromium embedder.
|
||||
|
|
|
@ -16,15 +16,10 @@
|
|||
|
||||
import expect from 'expect';
|
||||
import {TLSSocket} from 'tls';
|
||||
import {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
||||
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
||||
import {
|
||||
getTestState
|
||||
} from './mocha-utils.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
|
||||
describe('ignoreHTTPSErrors', function () {
|
||||
/* Note that this test creates its own browser rather than use
|
||||
|
|
|
@ -23,18 +23,35 @@ import {
|
|||
setupTestPageAndContextHooks,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describe('InjectedUtil tests', function () {
|
||||
describe('PuppeteerUtil tests', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
it('should work', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const handle = await page
|
||||
.mainFrame()
|
||||
.worlds[PUPPETEER_WORLD].evaluate(() => {
|
||||
return typeof InjectedUtil === 'object';
|
||||
});
|
||||
expect(handle).toBeTruthy();
|
||||
const world = page.mainFrame().worlds[PUPPETEER_WORLD];
|
||||
const value = await world.evaluate(PuppeteerUtil => {
|
||||
return typeof PuppeteerUtil === 'object';
|
||||
}, world.puppeteerUtil);
|
||||
expect(value).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,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
describeFailsFirefox,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
|
||||
|
@ -29,7 +28,7 @@ describe('input tests', function () {
|
|||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
describeFailsFirefox('input', function () {
|
||||
describe('input', function () {
|
||||
it('should upload the file', async () => {
|
||||
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 () => {
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
@ -325,7 +324,7 @@ describe('input tests', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describeFailsFirefox('FileChooser.cancel', function () {
|
||||
describe('FileChooser.cancel', function () {
|
||||
it('should cancel dialog', async () => {
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
|
|
@ -119,21 +119,18 @@ describe('Keyboard', function () {
|
|||
})
|
||||
).toBe('a');
|
||||
});
|
||||
it(
|
||||
'ElementHandle.press should support |text| option',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('ElementHandle.press should support |text| option', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
const textarea = (await page.$('textarea'))!;
|
||||
await textarea.press('a', {text: 'ё'});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return document.querySelector('textarea')!.value;
|
||||
})
|
||||
).toBe('ё');
|
||||
}
|
||||
);
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
const textarea = (await page.$('textarea'))!;
|
||||
await textarea.press('a', {text: 'ё'});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return document.querySelector('textarea')!.value;
|
||||
})
|
||||
).toBe('ё');
|
||||
});
|
||||
it('should send a character with sendCharacter', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
|
|
@ -24,12 +24,7 @@ import {TLSSocket} from 'tls';
|
|||
import {promisify} from 'util';
|
||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||
import {Product} from '../../lib/cjs/puppeteer/common/Product.js';
|
||||
import {
|
||||
getTestState,
|
||||
itChromeOnly,
|
||||
itFirefoxOnly,
|
||||
itOnlyRegularInstall,
|
||||
} from './mocha-utils.js';
|
||||
import {getTestState, itOnlyRegularInstall} from './mocha-utils.js';
|
||||
import utils from './utils.js';
|
||||
|
||||
const mkdtempAsync = promisify(fs.mkdtemp);
|
||||
|
@ -208,6 +203,11 @@ describe('Launcher specs', 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 () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
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
|
||||
await rmAsync(userDataDir).catch(() => {});
|
||||
});
|
||||
itChromeOnly('tmp profile should be cleaned up', async () => {
|
||||
it('tmp profile should be cleaned up', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
|
||||
// Set a custom test tmp dir so that we can validate that
|
||||
|
@ -279,7 +279,7 @@ describe('Launcher specs', function () {
|
|||
// Restore env var
|
||||
process.env['PUPPETEER_TMP_DIR'] = '';
|
||||
});
|
||||
itFirefoxOnly('userDataDir option restores preferences', async () => {
|
||||
it('userDataDir option restores preferences', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
|
||||
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
|
||||
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 userDataDir = await mkdtempAsync(TMP_FOLDER);
|
||||
|
@ -459,49 +459,43 @@ describe('Launcher specs', function () {
|
|||
await page.close();
|
||||
await browser.close();
|
||||
});
|
||||
itChromeOnly(
|
||||
'should filter out ignored default arguments in Chrome',
|
||||
async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
// Make sure we launch with `--enable-automation` by default.
|
||||
const defaultArgs = puppeteer.defaultArgs();
|
||||
const browser = await puppeteer.launch(
|
||||
Object.assign({}, defaultBrowserOptions, {
|
||||
// Ignore first and third default argument.
|
||||
ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
|
||||
})
|
||||
);
|
||||
const spawnargs = browser.process()!.spawnargs;
|
||||
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();
|
||||
it('should filter out ignored default arguments in Chrome', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
// Make sure we launch with `--enable-automation` by default.
|
||||
const defaultArgs = puppeteer.defaultArgs();
|
||||
const browser = await puppeteer.launch(
|
||||
Object.assign({}, defaultBrowserOptions, {
|
||||
// Ignore first and third default argument.
|
||||
ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
|
||||
})
|
||||
);
|
||||
const spawnargs = browser.process()!.spawnargs;
|
||||
if (!spawnargs) {
|
||||
throw new Error('spawnargs not present');
|
||||
}
|
||||
);
|
||||
itFirefoxOnly(
|
||||
'should filter out ignored default argument in Firefox',
|
||||
async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
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();
|
||||
});
|
||||
it('should filter out ignored default argument in Firefox', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
|
||||
const defaultArgs = puppeteer.defaultArgs();
|
||||
const browser = await puppeteer.launch(
|
||||
Object.assign({}, defaultBrowserOptions, {
|
||||
// Only the first argument is fixed, others are optional.
|
||||
ignoreDefaultArgs: [defaultArgs[0]!],
|
||||
})
|
||||
);
|
||||
const spawnargs = browser.process()!.spawnargs;
|
||||
if (!spawnargs) {
|
||||
throw new Error('spawnargs not present');
|
||||
}
|
||||
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
|
||||
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
|
||||
await browser.close();
|
||||
const defaultArgs = puppeteer.defaultArgs();
|
||||
const browser = await puppeteer.launch(
|
||||
Object.assign({}, defaultBrowserOptions, {
|
||||
// Only the first argument is fixed, others are optional.
|
||||
ignoreDefaultArgs: [defaultArgs[0]!],
|
||||
})
|
||||
);
|
||||
const spawnargs = browser.process()!.spawnargs;
|
||||
if (!spawnargs) {
|
||||
throw new Error('spawnargs not present');
|
||||
}
|
||||
);
|
||||
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 () {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
const browser = await puppeteer.launch(defaultBrowserOptions);
|
||||
|
@ -511,24 +505,21 @@ describe('Launcher specs', function () {
|
|||
expect(pages).toEqual(['about:blank']);
|
||||
await browser.close();
|
||||
});
|
||||
it(
|
||||
'should have custom URL when launching browser',
|
||||
async () => {
|
||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
it('should have custom URL when launching browser', async () => {
|
||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
|
||||
const options = Object.assign({}, defaultBrowserOptions);
|
||||
options.args = [server.EMPTY_PAGE].concat(options.args || []);
|
||||
const browser = await puppeteer.launch(options);
|
||||
const pages = await browser.pages();
|
||||
expect(pages.length).toBe(1);
|
||||
const page = pages[0]!;
|
||||
if (page.url() !== server.EMPTY_PAGE) {
|
||||
await page.waitForNavigation();
|
||||
}
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
await browser.close();
|
||||
const options = Object.assign({}, defaultBrowserOptions);
|
||||
options.args = [server.EMPTY_PAGE].concat(options.args || []);
|
||||
const browser = await puppeteer.launch(options);
|
||||
const pages = await browser.pages();
|
||||
expect(pages.length).toBe(1);
|
||||
const page = pages[0]!;
|
||||
if (page.url() !== server.EMPTY_PAGE) {
|
||||
await page.waitForNavigation();
|
||||
}
|
||||
);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
await browser.close();
|
||||
});
|
||||
it('should pass the timeout parameter to browser.waitForTarget', async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
const options = Object.assign({}, defaultBrowserOptions, {
|
||||
|
@ -614,24 +605,21 @@ describe('Launcher specs', function () {
|
|||
});
|
||||
expect(error.message).toContain('either pipe or debugging port');
|
||||
});
|
||||
itChromeOnly(
|
||||
'should launch Chrome properly with --no-startup-window and waitForInitialPage=false',
|
||||
async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
const options = {
|
||||
waitForInitialPage: false,
|
||||
// This is needed to prevent Puppeteer from adding an initial blank page.
|
||||
// See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
|
||||
ignoreDefaultArgs: true,
|
||||
...defaultBrowserOptions,
|
||||
args: ['--no-startup-window'],
|
||||
};
|
||||
const browser = await puppeteer.launch(options);
|
||||
const pages = await browser.pages();
|
||||
expect(pages.length).toBe(0);
|
||||
await browser.close();
|
||||
}
|
||||
);
|
||||
it('should launch Chrome properly with --no-startup-window and waitForInitialPage=false', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
const options = {
|
||||
waitForInitialPage: false,
|
||||
// This is needed to prevent Puppeteer from adding an initial blank page.
|
||||
// See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
|
||||
ignoreDefaultArgs: true,
|
||||
...defaultBrowserOptions,
|
||||
args: ['--no-startup-window'],
|
||||
};
|
||||
const browser = await puppeteer.launch(options);
|
||||
const pages = await browser.pages();
|
||||
expect(pages.length).toBe(0);
|
||||
await browser.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Puppeteer.launch', function () {
|
||||
|
@ -808,68 +796,62 @@ describe('Launcher specs', function () {
|
|||
.sort()
|
||||
).toEqual(['about:blank', server.EMPTY_PAGE]);
|
||||
});
|
||||
it(
|
||||
'should be able to reconnect to a disconnected browser',
|
||||
async () => {
|
||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
it('should be able to reconnect to a disconnected browser', async () => {
|
||||
const {server, puppeteer, defaultBrowserOptions} = getTestState();
|
||||
|
||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||
const page = await originalBrowser.newPage();
|
||||
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
||||
originalBrowser.disconnect();
|
||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||
const page = await originalBrowser.newPage();
|
||||
await page.goto(server.PREFIX + '/frames/nested-frames.html');
|
||||
originalBrowser.disconnect();
|
||||
|
||||
const browser = await puppeteer.connect({browserWSEndpoint});
|
||||
const pages = await browser.pages();
|
||||
const restoredPage = pages.find(page => {
|
||||
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
||||
})!;
|
||||
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
|
||||
'http://localhost:<PORT>/frames/nested-frames.html',
|
||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||
' http://localhost:<PORT>/frames/frame.html (dos)',
|
||||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||
]);
|
||||
expect(
|
||||
await restoredPage.evaluate(() => {
|
||||
return 7 * 8;
|
||||
})
|
||||
).toBe(56);
|
||||
await browser.close();
|
||||
}
|
||||
);
|
||||
const browser = await puppeteer.connect({browserWSEndpoint});
|
||||
const pages = await browser.pages();
|
||||
const restoredPage = pages.find(page => {
|
||||
return page.url() === server.PREFIX + '/frames/nested-frames.html';
|
||||
})!;
|
||||
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
|
||||
'http://localhost:<PORT>/frames/nested-frames.html',
|
||||
' http://localhost:<PORT>/frames/two-frames.html (2frames)',
|
||||
' http://localhost:<PORT>/frames/frame.html (uno)',
|
||||
' http://localhost:<PORT>/frames/frame.html (dos)',
|
||||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||
]);
|
||||
expect(
|
||||
await restoredPage.evaluate(() => {
|
||||
return 7 * 8;
|
||||
})
|
||||
).toBe(56);
|
||||
await browser.close();
|
||||
});
|
||||
// @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
|
||||
it(
|
||||
'should be able to connect to the same page simultaneously',
|
||||
async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
it('should be able to connect to the same page simultaneously', async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
|
||||
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserTwo = await puppeteer.connect({
|
||||
browserWSEndpoint: browserOne.wsEndpoint(),
|
||||
});
|
||||
const [page1, page2] = await Promise.all([
|
||||
new Promise<Page>(x => {
|
||||
return browserOne.once('targetcreated', target => {
|
||||
return x(target.page());
|
||||
});
|
||||
}),
|
||||
browserTwo.newPage(),
|
||||
]);
|
||||
expect(
|
||||
await page1.evaluate(() => {
|
||||
return 7 * 8;
|
||||
})
|
||||
).toBe(56);
|
||||
expect(
|
||||
await page2.evaluate(() => {
|
||||
return 7 * 6;
|
||||
})
|
||||
).toBe(42);
|
||||
await browserOne.close();
|
||||
}
|
||||
);
|
||||
const browserOne = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserTwo = await puppeteer.connect({
|
||||
browserWSEndpoint: browserOne.wsEndpoint(),
|
||||
});
|
||||
const [page1, page2] = await Promise.all([
|
||||
new Promise<Page>(x => {
|
||||
return browserOne.once('targetcreated', target => {
|
||||
return x(target.page());
|
||||
});
|
||||
}),
|
||||
browserTwo.newPage(),
|
||||
]);
|
||||
expect(
|
||||
await page1.evaluate(() => {
|
||||
return 7 * 8;
|
||||
})
|
||||
).toBe(56);
|
||||
expect(
|
||||
await page2.evaluate(() => {
|
||||
return 7 * 6;
|
||||
})
|
||||
).toBe(42);
|
||||
await browserOne.close();
|
||||
});
|
||||
it('should be able to reconnect', async () => {
|
||||
const {puppeteer, server, defaultBrowserOptions} = getTestState();
|
||||
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('and the executable exists', () => {
|
||||
itChromeOnly('returns /usr/bin/chromium-browser', async () => {
|
||||
it('returns /usr/bin/chromium-browser', async () => {
|
||||
const {puppeteer} = getTestState();
|
||||
const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
|
||||
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
||||
|
@ -971,26 +953,21 @@ describe('Launcher specs', function () {
|
|||
});
|
||||
});
|
||||
describe('and the executable does not exist', () => {
|
||||
itChromeOnly(
|
||||
'does not return /usr/bin/chromium-browser',
|
||||
async () => {
|
||||
const {puppeteer} = getTestState();
|
||||
const osPlatformStub = sinon
|
||||
.stub(os, 'platform')
|
||||
.returns('linux');
|
||||
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
|
||||
const fsExistsStub = sinon.stub(fs, 'existsSync');
|
||||
fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false);
|
||||
it('does not return /usr/bin/chromium-browser', async () => {
|
||||
const {puppeteer} = getTestState();
|
||||
const osPlatformStub = sinon.stub(os, 'platform').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();
|
||||
osArchStub.restore();
|
||||
fsExistsStub.restore();
|
||||
}
|
||||
);
|
||||
osPlatformStub.restore();
|
||||
osArchStub.restore();
|
||||
fsExistsStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1020,51 +997,48 @@ describe('Launcher specs', function () {
|
|||
});
|
||||
|
||||
describe('Browser.Events.disconnected', function () {
|
||||
it(
|
||||
'should be emitted when: browser gets closed, disconnected or underlying websocket gets closed',
|
||||
async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||
const remoteBrowser1 = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
});
|
||||
const remoteBrowser2 = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
});
|
||||
it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
|
||||
const {puppeteer, defaultBrowserOptions} = getTestState();
|
||||
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
|
||||
const browserWSEndpoint = originalBrowser.wsEndpoint();
|
||||
const remoteBrowser1 = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
});
|
||||
const remoteBrowser2 = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
});
|
||||
|
||||
let disconnectedOriginal = 0;
|
||||
let disconnectedRemote1 = 0;
|
||||
let disconnectedRemote2 = 0;
|
||||
originalBrowser.on('disconnected', () => {
|
||||
return ++disconnectedOriginal;
|
||||
});
|
||||
remoteBrowser1.on('disconnected', () => {
|
||||
return ++disconnectedRemote1;
|
||||
});
|
||||
remoteBrowser2.on('disconnected', () => {
|
||||
return ++disconnectedRemote2;
|
||||
});
|
||||
let disconnectedOriginal = 0;
|
||||
let disconnectedRemote1 = 0;
|
||||
let disconnectedRemote2 = 0;
|
||||
originalBrowser.on('disconnected', () => {
|
||||
return ++disconnectedOriginal;
|
||||
});
|
||||
remoteBrowser1.on('disconnected', () => {
|
||||
return ++disconnectedRemote1;
|
||||
});
|
||||
remoteBrowser2.on('disconnected', () => {
|
||||
return ++disconnectedRemote2;
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
utils.waitEvent(remoteBrowser2, 'disconnected'),
|
||||
remoteBrowser2.disconnect(),
|
||||
]);
|
||||
await Promise.all([
|
||||
utils.waitEvent(remoteBrowser2, 'disconnected'),
|
||||
remoteBrowser2.disconnect(),
|
||||
]);
|
||||
|
||||
expect(disconnectedOriginal).toBe(0);
|
||||
expect(disconnectedRemote1).toBe(0);
|
||||
expect(disconnectedRemote2).toBe(1);
|
||||
expect(disconnectedOriginal).toBe(0);
|
||||
expect(disconnectedRemote1).toBe(0);
|
||||
expect(disconnectedRemote2).toBe(1);
|
||||
|
||||
await Promise.all([
|
||||
utils.waitEvent(remoteBrowser1, 'disconnected'),
|
||||
utils.waitEvent(originalBrowser, 'disconnected'),
|
||||
originalBrowser.close(),
|
||||
]);
|
||||
await Promise.all([
|
||||
utils.waitEvent(remoteBrowser1, 'disconnected'),
|
||||
utils.waitEvent(originalBrowser, 'disconnected'),
|
||||
originalBrowser.close(),
|
||||
]);
|
||||
|
||||
expect(disconnectedOriginal).toBe(1);
|
||||
expect(disconnectedRemote1).toBe(1);
|
||||
expect(disconnectedRemote2).toBe(1);
|
||||
}
|
||||
);
|
||||
expect(disconnectedOriginal).toBe(1);
|
||||
expect(disconnectedRemote1).toBe(1);
|
||||
expect(disconnectedRemote2).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,14 +17,10 @@
|
|||
import Protocol from 'devtools-protocol';
|
||||
import expect from 'expect';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
||||
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||
import {
|
||||
|
@ -34,6 +30,7 @@ import {
|
|||
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
|
||||
import {TestServer} from '../../utils/testserver/lib/index.js';
|
||||
import {extendExpectWithToBeGolden} from './utils.js';
|
||||
import * as Mocha from 'mocha';
|
||||
|
||||
const setupServer = async () => {
|
||||
const assetsPath = path.join(__dirname, '../assets');
|
||||
|
@ -63,14 +60,15 @@ export const getTestState = (): PuppeteerTestState => {
|
|||
};
|
||||
|
||||
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 headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
|
||||
const isHeadless = headless === 'true' || headless === 'chrome';
|
||||
const isFirefox = product === 'firefox';
|
||||
const isChrome = product === 'Chromium';
|
||||
const isChrome = product === 'chrome';
|
||||
const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp';
|
||||
|
||||
let extraLaunchOptions = {};
|
||||
try {
|
||||
|
@ -91,6 +89,7 @@ const defaultBrowserOptions = Object.assign(
|
|||
executablePath: process.env['BINARY'],
|
||||
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
|
||||
dumpio: !!process.env['DUMPIO'],
|
||||
protocol: protocol as 'cdp' | 'webDriverBiDi',
|
||||
},
|
||||
extraLaunchOptions
|
||||
);
|
||||
|
@ -125,7 +124,11 @@ declare module 'expect/build/types' {
|
|||
}
|
||||
|
||||
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 OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
|
||||
if (fs.existsSync(OUTPUT_DIR)) {
|
||||
|
@ -152,116 +155,21 @@ interface 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 = (
|
||||
description: string,
|
||||
body: Mocha.Func
|
||||
body: Mocha.AsyncFunc
|
||||
): Mocha.Test => {
|
||||
if (alternativeInstall || process.env['BINARY']) {
|
||||
return xit(description, body);
|
||||
return it.skip(description, body);
|
||||
} else {
|
||||
return it(description, body);
|
||||
}
|
||||
};
|
||||
|
||||
export const itFailsWindowsUntilDate = (
|
||||
date: Date,
|
||||
description: string,
|
||||
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') {
|
||||
if (
|
||||
process.env['MOCHA_WORKER_ID'] === undefined ||
|
||||
process.env['MOCHA_WORKER_ID'] === '0'
|
||||
) {
|
||||
console.log(
|
||||
`Running unit tests with:
|
||||
-> product: ${product}
|
||||
|
@ -290,7 +198,7 @@ export const setupTestBrowserHooks = (): void => {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await state.browser!.close();
|
||||
await state.browser?.close();
|
||||
state.browser = undefined;
|
||||
});
|
||||
};
|
||||
|
@ -302,7 +210,7 @@ export const setupTestPageAndContextHooks = (): void => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await state.context!.close();
|
||||
await state.context?.close();
|
||||
state.context = 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');
|
||||
});
|
||||
it(
|
||||
'should trigger hover state with removed window.Node',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should trigger hover state with removed window.Node', 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(() => {
|
||||
// @ts-expect-error Expected.
|
||||
return delete window.Node;
|
||||
});
|
||||
await page.hover('#button-6');
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return document.querySelector('button:hover')!.id;
|
||||
})
|
||||
).toBe('button-6');
|
||||
}
|
||||
);
|
||||
return document.querySelector('button:hover')!.id;
|
||||
})
|
||||
).toBe('button-6');
|
||||
});
|
||||
it('should set modifier keys on click', async () => {
|
||||
const {page, server, isFirefox} = getTestState();
|
||||
|
||||
|
|
|
@ -122,28 +122,22 @@ describe('navigation', function () {
|
|||
const response = await page.goto(server.PREFIX + '/grid.html');
|
||||
expect(response!.status()).toBe(200);
|
||||
});
|
||||
it(
|
||||
'should navigate to empty page with networkidle0',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should navigate to empty page with networkidle0', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
const response = await page.goto(server.EMPTY_PAGE, {
|
||||
waitUntil: 'networkidle0',
|
||||
});
|
||||
expect(response!.status()).toBe(200);
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should navigate to empty page with networkidle2',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
const response = await page.goto(server.EMPTY_PAGE, {
|
||||
waitUntil: 'networkidle0',
|
||||
});
|
||||
expect(response!.status()).toBe(200);
|
||||
});
|
||||
it('should navigate to empty page with networkidle2', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
const response = await page.goto(server.EMPTY_PAGE, {
|
||||
waitUntil: 'networkidle2',
|
||||
});
|
||||
expect(response!.status()).toBe(200);
|
||||
}
|
||||
);
|
||||
const response = await page.goto(server.EMPTY_PAGE, {
|
||||
waitUntil: 'networkidle2',
|
||||
});
|
||||
expect(response!.status()).toBe(200);
|
||||
});
|
||||
it('should fail when navigating to bad url', async () => {
|
||||
const {page, isChrome} = getTestState();
|
||||
|
||||
|
@ -332,85 +326,79 @@ describe('navigation', function () {
|
|||
expect(response.ok()).toBe(true);
|
||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||
});
|
||||
it(
|
||||
'should wait for network idle to succeed navigation',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should wait for network idle to succeed navigation', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
let responses: ServerResponse[] = [];
|
||||
// Hold on to a bunch of requests without answering.
|
||||
server.setRoute('/fetch-request-a.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-b.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-c.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-d.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
const initialFetchResourcesRequested = Promise.all([
|
||||
server.waitForRequest('/fetch-request-a.js'),
|
||||
server.waitForRequest('/fetch-request-b.js'),
|
||||
server.waitForRequest('/fetch-request-c.js'),
|
||||
]);
|
||||
const secondFetchResourceRequested = server.waitForRequest(
|
||||
'/fetch-request-d.js'
|
||||
);
|
||||
let responses: ServerResponse[] = [];
|
||||
// Hold on to a bunch of requests without answering.
|
||||
server.setRoute('/fetch-request-a.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-b.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-c.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
server.setRoute('/fetch-request-d.js', (_req, res) => {
|
||||
return responses.push(res);
|
||||
});
|
||||
const initialFetchResourcesRequested = Promise.all([
|
||||
server.waitForRequest('/fetch-request-a.js'),
|
||||
server.waitForRequest('/fetch-request-b.js'),
|
||||
server.waitForRequest('/fetch-request-c.js'),
|
||||
]);
|
||||
const secondFetchResourceRequested = server.waitForRequest(
|
||||
'/fetch-request-d.js'
|
||||
);
|
||||
|
||||
// Navigate to a page which loads immediately and then does a bunch of
|
||||
// requests via javascript's fetch method.
|
||||
const navigationPromise = page.goto(
|
||||
server.PREFIX + '/networkidle.html',
|
||||
{
|
||||
waitUntil: 'networkidle0',
|
||||
}
|
||||
);
|
||||
// Track when the navigation gets completed.
|
||||
let navigationFinished = false;
|
||||
navigationPromise.then(() => {
|
||||
return (navigationFinished = true);
|
||||
});
|
||||
// Navigate to a page which loads immediately and then does a bunch of
|
||||
// requests via javascript's fetch method.
|
||||
const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', {
|
||||
waitUntil: 'networkidle0',
|
||||
});
|
||||
// Track when the navigation gets completed.
|
||||
let navigationFinished = false;
|
||||
navigationPromise.then(() => {
|
||||
return (navigationFinished = true);
|
||||
});
|
||||
|
||||
// Wait for the page's 'load' event.
|
||||
await new Promise(fulfill => {
|
||||
return page.once('load', fulfill);
|
||||
});
|
||||
expect(navigationFinished).toBe(false);
|
||||
// Wait for the page's 'load' event.
|
||||
await new Promise(fulfill => {
|
||||
return page.once('load', fulfill);
|
||||
});
|
||||
expect(navigationFinished).toBe(false);
|
||||
|
||||
// Wait for the initial three resources to be requested.
|
||||
await initialFetchResourcesRequested;
|
||||
// Wait for the initial three resources to be requested.
|
||||
await initialFetchResourcesRequested;
|
||||
|
||||
// Expect navigation still to be not finished.
|
||||
expect(navigationFinished).toBe(false);
|
||||
// Expect navigation still to be not finished.
|
||||
expect(navigationFinished).toBe(false);
|
||||
|
||||
// Respond to initial requests.
|
||||
for (const response of responses) {
|
||||
response.statusCode = 404;
|
||||
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);
|
||||
// Respond to initial requests.
|
||||
for (const response of responses) {
|
||||
response.statusCode = 404;
|
||||
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);
|
||||
});
|
||||
it('should not leak listeners during navigation', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
@ -459,38 +447,32 @@ describe('navigation', function () {
|
|||
process.removeListener('warning', warningHandler);
|
||||
expect(warning).toBe(null);
|
||||
});
|
||||
it(
|
||||
'should navigate to dataURL and fire dataURL requests',
|
||||
async () => {
|
||||
const {page} = getTestState();
|
||||
it('should navigate to dataURL and fire dataURL requests', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const requests: HTTPRequest[] = [];
|
||||
page.on('request', request => {
|
||||
return !utils.isFavicon(request) && requests.push(request);
|
||||
});
|
||||
const dataURL = 'data:text/html,<div>yo</div>';
|
||||
const response = (await page.goto(dataURL))!;
|
||||
expect(response.status()).toBe(200);
|
||||
expect(requests.length).toBe(1);
|
||||
expect(requests[0]!.url()).toBe(dataURL);
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should navigate to URL with hash and fire requests without hash',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
const requests: HTTPRequest[] = [];
|
||||
page.on('request', request => {
|
||||
return !utils.isFavicon(request) && requests.push(request);
|
||||
});
|
||||
const dataURL = 'data:text/html,<div>yo</div>';
|
||||
const response = (await page.goto(dataURL))!;
|
||||
expect(response.status()).toBe(200);
|
||||
expect(requests.length).toBe(1);
|
||||
expect(requests[0]!.url()).toBe(dataURL);
|
||||
});
|
||||
it('should navigate to URL with hash and fire requests without hash', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
const requests: HTTPRequest[] = [];
|
||||
page.on('request', request => {
|
||||
return !utils.isFavicon(request) && requests.push(request);
|
||||
});
|
||||
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
|
||||
expect(response.status()).toBe(200);
|
||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||
expect(requests.length).toBe(1);
|
||||
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||
}
|
||||
);
|
||||
const requests: HTTPRequest[] = [];
|
||||
page.on('request', request => {
|
||||
return !utils.isFavicon(request) && requests.push(request);
|
||||
});
|
||||
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
|
||||
expect(response.status()).toBe(200);
|
||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||
expect(requests.length).toBe(1);
|
||||
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
|
||||
});
|
||||
it('should work with self requesting page', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
@ -614,13 +596,11 @@ describe('navigation', function () {
|
|||
expect(response).toBe(null);
|
||||
expect(page.url()).toBe(server.PREFIX + '/replaced.html');
|
||||
});
|
||||
it(
|
||||
'should work with DOM history.back()/history.forward()',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('should work with DOM history.back()/history.forward()', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<a id=back onclick='javascript:goBack()'>back</a>
|
||||
<a id=forward onclick='javascript:goForward()'>forward</a>
|
||||
<script>
|
||||
|
@ -630,46 +610,42 @@ describe('navigation', function () {
|
|||
history.pushState({}, '', '/second.html');
|
||||
</script>
|
||||
`);
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
const [backResponse] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('a#back'),
|
||||
]);
|
||||
expect(backResponse).toBe(null);
|
||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||
const [forwardResponse] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('a#forward'),
|
||||
]);
|
||||
expect(forwardResponse).toBe(null);
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should work when subframe issues window.stop()',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
const [backResponse] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('a#back'),
|
||||
]);
|
||||
expect(backResponse).toBe(null);
|
||||
expect(page.url()).toBe(server.PREFIX + '/first.html');
|
||||
const [forwardResponse] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('a#forward'),
|
||||
]);
|
||||
expect(forwardResponse).toBe(null);
|
||||
expect(page.url()).toBe(server.PREFIX + '/second.html');
|
||||
});
|
||||
it('should work when subframe issues window.stop()', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
server.setRoute('/frames/style.css', () => {});
|
||||
const navigationPromise = page.goto(
|
||||
server.PREFIX + '/frames/one-frame.html'
|
||||
);
|
||||
const frame = await utils.waitEvent(page, 'frameattached');
|
||||
await new Promise<void>(fulfill => {
|
||||
page.on('framenavigated', f => {
|
||||
if (f === frame) {
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
server.setRoute('/frames/style.css', () => {});
|
||||
const navigationPromise = page.goto(
|
||||
server.PREFIX + '/frames/one-frame.html'
|
||||
);
|
||||
const frame = await utils.waitEvent(page, 'frameattached');
|
||||
await new Promise<void>(fulfill => {
|
||||
page.on('framenavigated', f => {
|
||||
if (f === frame) {
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
frame.evaluate(() => {
|
||||
return window.stop();
|
||||
}),
|
||||
navigationPromise,
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
await Promise.all([
|
||||
frame.evaluate(() => {
|
||||
return window.stop();
|
||||
}),
|
||||
navigationPromise,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.goBack', function () {
|
||||
|
|
|
@ -22,9 +22,6 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
itFailsFirefox,
|
||||
itChromeOnly,
|
||||
itFirefoxOnly,
|
||||
} from './mocha-utils.js';
|
||||
import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
|
||||
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
|
||||
|
@ -114,13 +111,13 @@ describe('network', 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 response = (await page.goto(server.EMPTY_PAGE))!;
|
||||
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 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('style.css').isNavigationRequest()).toBe(false);
|
||||
});
|
||||
// This `itFailsFirefox` should be preserved in mozilla-central (Firefox).
|
||||
// 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 () => {
|
||||
it('should work when navigating to image', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
const requests: HTTPRequest[] = [];
|
||||
|
|
|
@ -16,17 +16,11 @@
|
|||
|
||||
import utils from './utils.js';
|
||||
import expect from 'expect';
|
||||
import {
|
||||
getTestState,
|
||||
describeChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
import {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
} from '../../lib/cjs/puppeteer/common/Browser.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.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 */
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
|
@ -206,6 +200,7 @@ describeChromeOnly('OOPIF', function () {
|
|||
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
expect(frame.url()).toBe(server.EMPTY_PAGE);
|
||||
});
|
||||
|
||||
it('should support evaluating in oop iframes', async () => {
|
||||
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 {
|
||||
getTestState,
|
||||
itFailsFirefox,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
} from './mocha-utils.js';
|
||||
|
@ -546,39 +545,69 @@ describe('Page', function () {
|
|||
it('should work', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
// Instantiate an object
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).set = new Set(['hello', 'world']));
|
||||
// Create a custom class
|
||||
const classHandle = await page.evaluateHandle(() => {
|
||||
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
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).set = new Set(['hello', 'world']));
|
||||
});
|
||||
const prototypeHandle = await page.evaluateHandle(() => {
|
||||
return Set.prototype;
|
||||
});
|
||||
// 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);
|
||||
const count = await page.evaluate(objects => {
|
||||
return objects.length;
|
||||
}, objectsHandle);
|
||||
expect(count).toBe(1);
|
||||
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 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 () => {
|
||||
const {page} = getTestState();
|
||||
|
@ -1651,7 +1680,7 @@ describe('Page', function () {
|
|||
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
|
@ -1664,10 +1693,12 @@ describe('Page', function () {
|
|||
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
||||
type: 'module',
|
||||
});
|
||||
await page.waitForFunction('window.__es6injected');
|
||||
await page.waitForFunction(() => {
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
|
@ -1680,10 +1711,12 @@ describe('Page', function () {
|
|||
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
||||
type: 'module',
|
||||
});
|
||||
await page.waitForFunction('window.__es6injected');
|
||||
await page.waitForFunction(() => {
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
|
@ -1758,7 +1791,7 @@ describe('Page', function () {
|
|||
});
|
||||
|
||||
// @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();
|
||||
|
||||
await page.goto(server.PREFIX + '/csp.html');
|
||||
|
@ -1854,7 +1887,7 @@ describe('Page', function () {
|
|||
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
||||
});
|
||||
const styleHandle = (await page.$('style'))!;
|
||||
const styleContent = await page.evaluate(style => {
|
||||
const styleContent = await page.evaluate((style: HTMLStyleElement) => {
|
||||
return style.innerHTML;
|
||||
}, styleHandle);
|
||||
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
||||
|
@ -2002,10 +2035,7 @@ describe('Page', function () {
|
|||
expect(size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// This test should be skipped in mozilla-central (Firefox).
|
||||
// It intermittently makes the whole test suite fail.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255
|
||||
itFailsFirefox('should respect timeout', async () => {
|
||||
it('should respect timeout', async () => {
|
||||
const {isHeadless, page, server, puppeteer} = getTestState();
|
||||
if (!isHeadless) {
|
||||
return;
|
||||
|
@ -2236,7 +2266,7 @@ describe('Page', 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 newPagePromise = new Promise<Page>(fulfill => {
|
||||
|
|
|
@ -17,13 +17,9 @@
|
|||
import expect from 'expect';
|
||||
import http from 'http';
|
||||
import os from 'os';
|
||||
import {
|
||||
getTestState,
|
||||
describeFailsFirefox,
|
||||
itFailsWindows,
|
||||
} from './mocha-utils.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
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 {TestServer} from '../../utils/testserver/lib/index.js';
|
||||
|
||||
|
@ -53,7 +49,7 @@ function getEmptyPageUrl(server: TestServer): string {
|
|||
return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
|
||||
}
|
||||
|
||||
describeFailsFirefox('request proxy', () => {
|
||||
describe('request proxy', () => {
|
||||
let browser: Browser;
|
||||
let proxiedRequestUrls: string[];
|
||||
let proxyServer: Server;
|
||||
|
@ -194,28 +190,25 @@ describeFailsFirefox('request proxy', () => {
|
|||
/**
|
||||
* See issues #7873, #7719, and #7698.
|
||||
*/
|
||||
itFailsWindows(
|
||||
'should proxy requests when configured at context level',
|
||||
async () => {
|
||||
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
||||
const emptyPageUrl = getEmptyPageUrl(server);
|
||||
it('should proxy requests when configured at context level', async () => {
|
||||
const {puppeteer, defaultBrowserOptions, server} = getTestState();
|
||||
const emptyPageUrl = getEmptyPageUrl(server);
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
...defaultBrowserOptions,
|
||||
args: defaultArgs,
|
||||
});
|
||||
browser = await puppeteer.launch({
|
||||
...defaultBrowserOptions,
|
||||
args: defaultArgs,
|
||||
});
|
||||
|
||||
const context = await browser.createIncognitoBrowserContext({
|
||||
proxyServer: proxyServerUrl,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const response = (await page.goto(emptyPageUrl))!;
|
||||
const context = await browser.createIncognitoBrowserContext({
|
||||
proxyServer: proxyServerUrl,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
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 () => {
|
||||
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('in Page', function () {
|
||||
it('should query existing element', async () => {
|
||||
|
|
|
@ -19,8 +19,6 @@ import {
|
|||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
itHeadfulOnly,
|
||||
itChromeOnly,
|
||||
} from './mocha-utils.js';
|
||||
|
||||
describe('Screenshots', function () {
|
||||
|
@ -67,23 +65,20 @@ describe('Screenshots', function () {
|
|||
});
|
||||
expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
|
||||
});
|
||||
it(
|
||||
'should get screenshot bigger than the viewport',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
await page.setViewport({width: 50, height: 50});
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
const screenshot = await page.screenshot({
|
||||
clip: {
|
||||
x: 25,
|
||||
y: 25,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
});
|
||||
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
||||
}
|
||||
);
|
||||
it('should get screenshot bigger than the viewport', async () => {
|
||||
const {page, server} = getTestState();
|
||||
await page.setViewport({width: 50, height: 50});
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
const screenshot = await page.screenshot({
|
||||
clip: {
|
||||
x: 25,
|
||||
y: 25,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
});
|
||||
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
|
||||
});
|
||||
it('should run in parallel', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
|
@ -205,7 +200,7 @@ describe('Screenshots', function () {
|
|||
'screenshot-sanity.png'
|
||||
);
|
||||
});
|
||||
itHeadfulOnly('should work in "fromSurface: false" mode', async () => {
|
||||
it('should work in "fromSurface: false" mode', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
|
@ -230,7 +225,7 @@ describe('Screenshots', function () {
|
|||
const screenshot = await elementHandle.screenshot();
|
||||
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 browser = await puppeteer.launch({
|
||||
|
@ -270,14 +265,12 @@ describe('Screenshots', function () {
|
|||
const screenshot = await elementHandle.screenshot();
|
||||
expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
|
||||
});
|
||||
it(
|
||||
'should capture full element when larger than viewport',
|
||||
async () => {
|
||||
const {page} = getTestState();
|
||||
it('should capture full element when larger than viewport', 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
|
||||
<style>
|
||||
div.to-screenshot {
|
||||
|
@ -292,22 +285,21 @@ describe('Screenshots', function () {
|
|||
</style>
|
||||
<div class="to-screenshot"></div>
|
||||
`);
|
||||
const elementHandle = (await page.$('div.to-screenshot'))!;
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
expect(screenshot).toBeGolden(
|
||||
'screenshot-element-larger-than-viewport.png'
|
||||
);
|
||||
const elementHandle = (await page.$('div.to-screenshot'))!;
|
||||
const screenshot = await elementHandle.screenshot();
|
||||
expect(screenshot).toBeGolden(
|
||||
'screenshot-element-larger-than-viewport.png'
|
||||
);
|
||||
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return {
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
})
|
||||
).toEqual({w: 500, h: 500});
|
||||
}
|
||||
);
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return {
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
})
|
||||
).toEqual({w: 500, h: 500});
|
||||
});
|
||||
it('should scroll element into view', async () => {
|
||||
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 {
|
||||
getTestState,
|
||||
itFailsFirefox,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
} from './mocha-utils.js';
|
||||
import utils from './utils.js';
|
||||
|
||||
const {waitEvent} = utils;
|
||||
|
||||
describe('Target', function () {
|
||||
|
@ -79,10 +79,7 @@ describe('Target', function () {
|
|||
).toBe('Hello world');
|
||||
expect(await originalPage.$('body')).toBeTruthy();
|
||||
});
|
||||
// This test should be skipped in mozilla-central (Firefox).
|
||||
// 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 () => {
|
||||
it('should be able to use async waitForTarget', async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
|
||||
const [otherPage] = await Promise.all([
|
||||
|
@ -104,88 +101,82 @@ describe('Target', function () {
|
|||
);
|
||||
expect(page).not.toEqual(otherPage);
|
||||
});
|
||||
it(
|
||||
'should report when a new page is created and closed',
|
||||
async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
it('should report when a new page is created and closed', async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
|
||||
const [otherPage] = await Promise.all([
|
||||
context
|
||||
.waitForTarget(target => {
|
||||
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(' ');
|
||||
const [otherPage] = await Promise.all([
|
||||
context
|
||||
.waitForTarget(target => {
|
||||
return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
|
||||
})
|
||||
).toBe('Hello world');
|
||||
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 => {
|
||||
.then(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();
|
||||
}),
|
||||
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');
|
||||
expect(await otherPage!.$('body')).toBeTruthy();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const createdTarget = new Promise<Target>(fulfill => {
|
||||
return context.once('targetcreated', target => {
|
||||
return fulfill(target);
|
||||
});
|
||||
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);
|
||||
|
||||
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');
|
||||
expect((await createdTarget).url()).toBe(
|
||||
server.PREFIX + '/serviceworkers/empty/sw.js'
|
||||
);
|
||||
|
||||
const destroyedTarget = new Promise(fulfill => {
|
||||
return context.once('targetdestroyed', target => {
|
||||
return fulfill(target);
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const createdTarget = new Promise<Target>(fulfill => {
|
||||
return context.once('targetcreated', target => {
|
||||
return fulfill(target);
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
return (
|
||||
globalThis as unknown as {
|
||||
registrationPromise: Promise<{unregister: () => void}>;
|
||||
}
|
||||
).registrationPromise.then((registration: any) => {
|
||||
return registration.unregister();
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
|
||||
|
||||
expect((await createdTarget).type()).toBe('service_worker');
|
||||
expect((await createdTarget).url()).toBe(
|
||||
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 () => {
|
||||
const {page, server, context} = getTestState();
|
||||
|
||||
|
@ -271,36 +262,33 @@ describe('Target', function () {
|
|||
expect(targetChanged).toBe(false);
|
||||
context.removeListener('targetchanged', listener);
|
||||
});
|
||||
it(
|
||||
'should not crash while redirecting if original request was missed',
|
||||
async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
it('should not crash while redirecting if original request was missed', async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
|
||||
let serverResponse!: ServerResponse;
|
||||
server.setRoute('/one-style.css', (_req, res) => {
|
||||
return (serverResponse = res);
|
||||
});
|
||||
// Open a new page. Use window.open to connect to the page later.
|
||||
await Promise.all([
|
||||
page.evaluate((url: string) => {
|
||||
return window.open(url);
|
||||
}, server.PREFIX + '/one-style.html'),
|
||||
server.waitForRequest('/one-style.css'),
|
||||
]);
|
||||
// Connect to the opened page.
|
||||
const target = await context.waitForTarget(target => {
|
||||
return target.url().includes('one-style.html');
|
||||
});
|
||||
const newPage = (await target.page())!;
|
||||
// Issue a redirect.
|
||||
serverResponse.writeHead(302, {location: '/injectedstyle.css'});
|
||||
serverResponse.end();
|
||||
// Wait for the new page to load.
|
||||
await waitEvent(newPage, 'load');
|
||||
// Cleanup.
|
||||
await newPage.close();
|
||||
}
|
||||
);
|
||||
let serverResponse!: ServerResponse;
|
||||
server.setRoute('/one-style.css', (_req, res) => {
|
||||
return (serverResponse = res);
|
||||
});
|
||||
// Open a new page. Use window.open to connect to the page later.
|
||||
await Promise.all([
|
||||
page.evaluate((url: string) => {
|
||||
return window.open(url);
|
||||
}, server.PREFIX + '/one-style.html'),
|
||||
server.waitForRequest('/one-style.css'),
|
||||
]);
|
||||
// Connect to the opened page.
|
||||
const target = await context.waitForTarget(target => {
|
||||
return target.url().includes('one-style.html');
|
||||
});
|
||||
const newPage = (await target.page())!;
|
||||
// Issue a redirect.
|
||||
serverResponse.writeHead(302, {location: '/injectedstyle.css'});
|
||||
serverResponse.end();
|
||||
// Wait for the new page to load.
|
||||
await waitEvent(newPage, 'load');
|
||||
// Cleanup.
|
||||
await newPage.close();
|
||||
});
|
||||
it('should have an opener', async () => {
|
||||
const {page, server, context} = getTestState();
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import expect from 'expect';
|
||||
import {getTestState, describeChromeOnly} from './mocha-utils.js';
|
||||
import {Browser} from '../../lib/cjs/puppeteer/common/Browser.js';
|
||||
import {getTestState} from './mocha-utils.js';
|
||||
import {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
|
||||
import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
|
||||
|
||||
describeChromeOnly('Tracing', function () {
|
||||
describe('Tracing', function () {
|
||||
let outputFile!: string;
|
||||
let browser!: Browser;
|
||||
let page!: Page;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
import expect from 'expect';
|
||||
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
|
||||
import {
|
||||
createTimeout,
|
||||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
|
@ -31,9 +32,9 @@ describe('waittask specs', function () {
|
|||
it('should accept a string', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const watchdog = page.waitForFunction('window.__FOO === 1');
|
||||
const watchdog = page.waitForFunction('self.__FOO === 1');
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).__FOO = 1);
|
||||
return ((self as unknown as {__FOO: number}).__FOO = 1);
|
||||
});
|
||||
await watchdog;
|
||||
});
|
||||
|
@ -46,61 +47,25 @@ describe('waittask specs', function () {
|
|||
await page.waitForFunction(() => {
|
||||
if (!(globalThis as any).__RELOADED) {
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
it('should poll on interval', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
let success = false;
|
||||
const startTime = Date.now();
|
||||
const polling = 100;
|
||||
const watchdog = page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
return (globalThis as any).__FOO === 'hit';
|
||||
},
|
||||
{
|
||||
polling,
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
return (success = true);
|
||||
});
|
||||
const watchdog = page.waitForFunction(
|
||||
() => {
|
||||
return (globalThis as any).__FOO === 'hit';
|
||||
},
|
||||
{polling}
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).__FOO = 'hit');
|
||||
});
|
||||
expect(success).toBe(false);
|
||||
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'));
|
||||
setTimeout(() => {
|
||||
(globalThis as any).__FOO = 'hit';
|
||||
}, 50);
|
||||
});
|
||||
await watchdog;
|
||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
||||
|
@ -212,26 +177,6 @@ describe('waittask specs', function () {
|
|||
]);
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
|
@ -299,23 +244,34 @@ describe('waittask specs', function () {
|
|||
const {page, puppeteer} = getTestState();
|
||||
|
||||
let error!: Error;
|
||||
await page.waitForFunction('false', {timeout: 10}).catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
return false;
|
||||
},
|
||||
{timeout: 10}
|
||||
)
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const {page, puppeteer} = getTestState();
|
||||
|
||||
page.setDefaultTimeout(1);
|
||||
let error!: Error;
|
||||
await page.waitForFunction('false').catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
await page
|
||||
.waitForFunction(() => {
|
||||
return false;
|
||||
})
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
@ -341,7 +297,9 @@ describe('waittask specs', function () {
|
|||
|
||||
let fooFound = false;
|
||||
const waitForFunction = page
|
||||
.waitForFunction('globalThis.__FOO === 1')
|
||||
.waitForFunction(() => {
|
||||
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
|
||||
})
|
||||
.then(() => {
|
||||
return (fooFound = true);
|
||||
});
|
||||
|
@ -464,21 +422,18 @@ describe('waittask specs', function () {
|
|||
await watchdog;
|
||||
});
|
||||
|
||||
it(
|
||||
'Page.waitForSelector is shortcut for main frame',
|
||||
async () => {
|
||||
const {page, server} = getTestState();
|
||||
it('Page.waitForSelector is shortcut for main frame', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const otherFrame = page.frames()[1]!;
|
||||
const watchdog = page.waitForSelector('div');
|
||||
await otherFrame.evaluate(addElement, 'div');
|
||||
await page.evaluate(addElement, 'div');
|
||||
const eHandle = await watchdog;
|
||||
expect(eHandle?.frame).toBe(page.mainFrame());
|
||||
}
|
||||
);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const otherFrame = page.frames()[1]!;
|
||||
const watchdog = page.waitForSelector('div');
|
||||
await otherFrame.evaluate(addElement, 'div');
|
||||
await page.evaluate(addElement, 'div');
|
||||
const eHandle = await watchdog;
|
||||
expect(eHandle?.frame).toBe(page.mainFrame());
|
||||
});
|
||||
|
||||
it('should run in specified frame', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
@ -525,113 +480,186 @@ describe('waittask specs', function () {
|
|||
await waitForSelector;
|
||||
expect(boxFound).toBe(true);
|
||||
});
|
||||
it('should wait for visible', async () => {
|
||||
it('should wait for element to be visible (display)', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
let divFound = false;
|
||||
const waitForSelector = page
|
||||
.waitForSelector('div', {visible: true})
|
||||
.then(() => {
|
||||
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');
|
||||
const promise = page.waitForSelector('div', {visible: true});
|
||||
await page.setContent('<div style="display: none">text</div>');
|
||||
const element = await page.evaluateHandle(() => {
|
||||
return document.getElementsByTagName('div')[0]!;
|
||||
});
|
||||
expect(divFound).toBe(false);
|
||||
await page.evaluate(() => {
|
||||
return document
|
||||
.querySelector('div')
|
||||
?.style.removeProperty('visibility');
|
||||
await expect(
|
||||
Promise.race([promise, createTimeout(40)])
|
||||
).resolves.toBeFalsy();
|
||||
await element.evaluate(e => {
|
||||
e.style.removeProperty('display');
|
||||
});
|
||||
expect(await waitForSelector).toBe(true);
|
||||
expect(divFound).toBe(true);
|
||||
await expect(promise).resolves.toBeTruthy();
|
||||
});
|
||||
it('should wait for visible recursively', async () => {
|
||||
it('should wait for element to be visible (visibility)', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
let divVisible = false;
|
||||
const waitForSelector = page
|
||||
.waitForSelector('div#inner', {visible: true})
|
||||
.then(() => {
|
||||
return (divVisible = true);
|
||||
});
|
||||
const promise = page.waitForSelector('div', {visible: true});
|
||||
await page.setContent('<div style="visibility: hidden">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('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(
|
||||
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
|
||||
);
|
||||
expect(divVisible).toBe(false);
|
||||
await page.evaluate(() => {
|
||||
return document.querySelector('div')?.style.removeProperty('display');
|
||||
const element = await page.evaluateHandle(() => {
|
||||
return document.getElementsByTagName('div')[0]!;
|
||||
});
|
||||
expect(divVisible).toBe(false);
|
||||
await page.evaluate(() => {
|
||||
return document
|
||||
.querySelector('div')
|
||||
?.style.removeProperty('visibility');
|
||||
await expect(
|
||||
Promise.race([promise, createTimeout(40)])
|
||||
).resolves.toBeFalsy();
|
||||
await element.evaluate(e => {
|
||||
return e.style.removeProperty('display');
|
||||
});
|
||||
expect(await waitForSelector).toBe(true);
|
||||
expect(divVisible).toBe(true);
|
||||
await expect(
|
||||
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();
|
||||
|
||||
let divHidden = false;
|
||||
await page.setContent(`<div style='display: block;'></div>`);
|
||||
const waitForSelector = page
|
||||
.waitForSelector('div', {hidden: true})
|
||||
.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');
|
||||
const promise = page.waitForSelector('div', {hidden: true});
|
||||
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||
const element = await page.evaluateHandle(() => {
|
||||
return document.getElementsByTagName('div')[0]!;
|
||||
});
|
||||
expect(await waitForSelector).toBe(true);
|
||||
expect(divHidden).toBe(true);
|
||||
await expect(
|
||||
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();
|
||||
|
||||
let divHidden = false;
|
||||
await page.setContent(`<div style='display: block;'></div>`);
|
||||
const waitForSelector = page
|
||||
.waitForSelector('div', {hidden: true})
|
||||
.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');
|
||||
const promise = page.waitForSelector('div', {hidden: true});
|
||||
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||
const element = await page.evaluateHandle(() => {
|
||||
return document.getElementsByTagName('div')[0]!;
|
||||
});
|
||||
expect(await waitForSelector).toBe(true);
|
||||
expect(divHidden).toBe(true);
|
||||
await expect(
|
||||
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();
|
||||
|
||||
await page.setContent(`<div></div>`);
|
||||
let divRemoved = false;
|
||||
const waitForSelector = page
|
||||
.waitForSelector('div', {hidden: true})
|
||||
.then(() => {
|
||||
return (divRemoved = true);
|
||||
});
|
||||
await page.waitForSelector('div'); // do a round trip
|
||||
expect(divRemoved).toBe(false);
|
||||
await page.evaluate(() => {
|
||||
return document.querySelector('div')?.remove();
|
||||
const promise = page.waitForSelector('div', {hidden: true});
|
||||
await page.setContent('<div>text</div>');
|
||||
const element = await page.evaluateHandle(() => {
|
||||
return document.getElementsByTagName('div')[0]!;
|
||||
});
|
||||
expect(await waitForSelector).toBe(true);
|
||||
expect(divRemoved).toBe(true);
|
||||
await expect(
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
@ -650,13 +678,13 @@ describe('waittask specs', function () {
|
|||
});
|
||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
await page.setContent(`<div></div>`);
|
||||
await page.setContent(`<div>text</div>`);
|
||||
let error!: Error;
|
||||
await page
|
||||
.waitForSelector('div', {hidden: true, timeout: 10})
|
||||
|
@ -665,7 +693,7 @@ describe('waittask specs', function () {
|
|||
});
|
||||
expect(error).toBeTruthy();
|
||||
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_ => {
|
||||
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.
|
||||
expect(error?.stack).toContain('waittask.spec.ts');
|
||||
expect(error?.stack).toContain('WaitTask.ts');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -733,9 +763,7 @@ describe('waittask specs', function () {
|
|||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||
expect(error?.message).toContain(
|
||||
'waiting for selector `.//div` failed: timeout 10ms exceeded'
|
||||
);
|
||||
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||
});
|
||||
it('should run in specified frame', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
@ -772,7 +800,7 @@ describe('waittask specs', function () {
|
|||
const {page} = getTestState();
|
||||
|
||||
let divHidden = false;
|
||||
await page.setContent(`<div style='display: block;'></div>`);
|
||||
await page.setContent(`<div style='display: block;'>text</div>`);
|
||||
const waitForXPath = page
|
||||
.waitForXPath('//div', {hidden: true})
|
||||
.then(() => {
|
||||
|
|
|
@ -18,14 +18,13 @@ import expect from 'expect';
|
|||
import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
|
||||
import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js';
|
||||
import {
|
||||
describeFailsFirefox,
|
||||
getTestState,
|
||||
setupTestBrowserHooks,
|
||||
setupTestPageAndContextHooks,
|
||||
} from './mocha-utils.js';
|
||||
import {waitEvent} from './utils.js';
|
||||
|
||||
describeFailsFirefox('Workers', function () {
|
||||
describe('Workers', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
it('Page.workers', async () => {
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{"path": "../tsconfig.lib.json"},
|
||||
{"path": "../utils/testserver/tsconfig.json"}
|
||||
{"path": "../src/tsconfig.cjs.json"},
|
||||
{"path": "../utils/testserver/tsconfig.json"},
|
||||
{"path": "../utils/mochaRunner/tsconfig.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 {job} from './internal/job.js';
|
||||
|
||||
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
|
||||
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util', 'api'];
|
||||
|
||||
(async () => {
|
||||
await job('', async ({outputs}) => {
|
||||
|
@ -36,7 +36,7 @@ const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
|
|||
outdir: tmp,
|
||||
format: 'cjs',
|
||||
platform: 'browser',
|
||||
target: 'ES2019',
|
||||
target: 'ES2022',
|
||||
});
|
||||
const baseName = path.basename(input);
|
||||
const content = await readFile(
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче