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:
Alexandra Borovova 2023-01-17 10:42:50 +00:00
parent b306cf161f
commit d115e24d80
109 changed files with 7883 additions and 4919 deletions

View file

@ -126,6 +126,8 @@ _OPT\.OBJ/
^remote/test/puppeteer/test/build ^remote/test/puppeteer/test/build
^remote/test/puppeteer/test/output-firefox ^remote/test/puppeteer/test/output-firefox
^remote/test/puppeteer/test/output-chromium ^remote/test/puppeteer/test/output-chromium
^remote/test/puppeteer/testserver/lib/
^remote/test/puppeteer/utils/mochaRunner/lib/
^remote/test/puppeteer/website ^remote/test/puppeteer/website
# git checkout of libstagefright # git checkout of libstagefright

2
remote/.gitignore vendored
View file

@ -15,4 +15,6 @@ test/puppeteer/src/generated
test/puppeteer/test/build test/puppeteer/test/build
test/puppeteer/test/output-firefox test/puppeteer/test/output-firefox
test/puppeteer/test/output-chromium test/puppeteer/test/output-chromium
test/puppeteer/testserver/lib/
test/puppeteer/utils/mochaRunner/lib/
test/puppeteer/website test/puppeteer/website

View file

@ -17,7 +17,6 @@ import mozprofile
from mach.decorators import Command, CommandArgument, SubCommand from mach.decorators import Command, CommandArgument, SubCommand
from mozbuild import nodeutil from mozbuild import nodeutil
from mozbuild.base import BinaryNotFoundException, MozbuildObject from mozbuild.base import BinaryNotFoundException, MozbuildObject
from six import iteritems
EX_CONFIG = 78 EX_CONFIG = 78
EX_SOFTWARE = 70 EX_SOFTWARE = 70
@ -261,8 +260,10 @@ class MochaOutputHandler(object):
if not status and not test_start: if not status and not test_start:
return return
test_info = event[1] test_info = event[1]
test_name = test_info.get("fullTitle", "") test_full_title = test_info.get("fullTitle", "")
test_name = test_full_title
test_path = test_info.get("file", "") test_path = test_info.get("file", "")
test_file_name = os.path.basename(test_path).replace(".js", "")
test_err = test_info.get("err") test_err = test_info.get("err")
if status == "FAIL" and test_err: if status == "FAIL" and test_err:
if "timeout" in test_err.lower(): if "timeout" in test_err.lower():
@ -276,7 +277,32 @@ class MochaOutputHandler(object):
if test_start: if test_start:
self.logger.test_start(test_name) self.logger.test_start(test_name)
return return
expected = self.expected.get(test_name, ["PASS"]) expected_name = "[{}] {}".format(test_file_name, test_full_title)
expected_item = next(
(
expectation
for expectation in list(self.expected)
if expectation["testIdPattern"] == expected_name
),
None,
)
if expected_item is None:
# if there is no expectation data for a specific test case,
# try to find data for a whole file.
expected_item_for_file = next(
(
expectation
for expectation in list(self.expected)
if expectation["testIdPattern"] == f"[{test_file_name}]"
),
None,
)
if expected_item_for_file is None:
expected = ["PASS"]
else:
expected = expected_item_for_file["expectations"]
else:
expected = expected_item["expectations"]
# mozlog doesn't really allow unexpected skip, # mozlog doesn't really allow unexpected skip,
# so if a test is disabled just expect that and note the unexpected skip # so if a test is disabled just expect that and note the unexpected skip
# Also, mocha doesn't log test-start for skipped tests # Also, mocha doesn't log test-start for skipped tests
@ -308,34 +334,7 @@ class MochaOutputHandler(object):
known_intermittent=known_intermittent, known_intermittent=known_intermittent,
) )
def new_expected(self): def after_end(self):
new_expected = OrderedDict()
for test_name, status in iteritems(self.test_results):
if test_name not in self.expected:
new_status = [status]
else:
if status in self.expected[test_name]:
new_status = self.expected[test_name]
else:
new_status = [status]
new_expected[test_name] = new_status
return new_expected
def after_end(self, subset=False):
if not subset:
missing = set(self.expected) - set(self.test_results)
extra = set(self.test_results) - set(self.expected)
if missing:
self.has_unexpected = True
for test_name in missing:
self.logger.error("TEST-UNEXPECTED-MISSING %s" % (test_name,))
if self.expected and extra:
self.has_unexpected = True
for test_name in extra:
self.logger.error(
"TEST-UNEXPECTED-MISSING Unknown new test %s" % (test_name,)
)
if self.unexpected_skips: if self.unexpected_skips:
self.has_unexpected = True self.has_unexpected = True
for test_name in self.unexpected_skips: for test_name in self.unexpected_skips:
@ -392,8 +391,6 @@ class PuppeteerRunner(MozbuildObject):
before invoking npm. Overrides default preferences. before invoking npm. Overrides default preferences.
`enable_webrender`: `enable_webrender`:
Boolean to indicate whether to enable WebRender compositor in Gecko. Boolean to indicate whether to enable WebRender compositor in Gecko.
`write_results`:
Path to write the results json file
`subset` `subset`
Indicates only a subset of tests are being run, so we should Indicates only a subset of tests are being run, so we should
skip the check for missing results skip the check for missing results
@ -425,6 +422,7 @@ class PuppeteerRunner(MozbuildObject):
"--timeout", "--timeout",
"20000", "20000",
"--no-parallel", "--no-parallel",
"--no-coverage",
] ]
env["HEADLESS"] = str(params.get("headless", False)) env["HEADLESS"] = str(params.get("headless", False))
@ -454,15 +452,31 @@ class PuppeteerRunner(MozbuildObject):
env["EXTRA_LAUNCH_OPTIONS"] = json.dumps(extra_options) env["EXTRA_LAUNCH_OPTIONS"] = json.dumps(extra_options)
expected_path = os.path.join( expected_path = os.path.join(
os.path.dirname(__file__), "test", "puppeteer-expected.json" os.path.dirname(__file__),
"test",
"puppeteer",
"test",
"TestExpectations.json",
) )
if product == "firefox" and os.path.exists(expected_path): if os.path.exists(expected_path):
with open(expected_path) as f: with open(expected_path) as f:
expected_data = json.load(f) expected_data = json.load(f)
else: else:
expected_data = {} expected_data = []
# Filter expectation data for the selected browser,
# headless or headful mode, and the operating system.
platform = os.uname().sysname.lower() if os.uname() else "win32"
expectations = filter(
lambda el: product in el["parameters"]
and (
(env["HEADLESS"] == "False" and "headless" not in el["parameters"])
or "headful" not in el["parameters"]
)
and platform in el["platforms"],
expected_data,
)
output_handler = MochaOutputHandler(logger, expected_data) output_handler = MochaOutputHandler(logger, list(expectations))
proc = npm( proc = npm(
*command, *command,
cwd=self.puppeteer_dir, cwd=self.puppeteer_dir,
@ -476,7 +490,7 @@ class PuppeteerRunner(MozbuildObject):
# failure, so use an output_timeout as a fallback # failure, so use an output_timeout as a fallback
wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False) wait_proc(proc, "npm", output_timeout=60, exit_on_fail=False)
output_handler.after_end(params.get("subset", False)) output_handler.after_end()
# Non-zero return codes are non-fatal for now since we have some # Non-zero return codes are non-fatal for now since we have some
# issues with unresolved promises that shouldn't otherwise block # issues with unresolved promises that shouldn't otherwise block
@ -484,12 +498,6 @@ class PuppeteerRunner(MozbuildObject):
if proc.returncode != 0: if proc.returncode != 0:
logger.warning("npm exited with code %s" % proc.returncode) logger.warning("npm exited with code %s" % proc.returncode)
if params["write_results"]:
with open(params["write_results"], "w") as f:
json.dump(
output_handler.new_expected(), f, indent=2, separators=(",", ": ")
)
if output_handler.has_unexpected: if output_handler.has_unexpected:
exit(1, "Got unexpected results") exit(1, "Got unexpected results")
@ -547,18 +555,6 @@ def create_parser_puppeteer():
"debug level messages with -v, trace messages with -vv," "debug level messages with -v, trace messages with -vv,"
"and to not truncate long trace messages with -vvv", "and to not truncate long trace messages with -vvv",
) )
p.add_argument(
"--write-results",
action="store",
nargs="?",
default=None,
const=os.path.join(
os.path.dirname(__file__), "test", "puppeteer-expected.json"
),
help="Path to write updated results to (defaults to the "
"expectations file if the argument is provided but "
"no path is passed)",
)
p.add_argument( p.add_argument(
"--subset", "--subset",
action="store_true", action="store_true",
@ -597,7 +593,6 @@ def puppeteer_test(
verbosity=0, verbosity=0,
tests=None, tests=None,
product="firefox", product="firefox",
write_results=None,
subset=False, subset=False,
**kwargs, **kwargs,
): ):
@ -657,7 +652,6 @@ def puppeteer_test(
"extra_prefs": prefs, "extra_prefs": prefs,
"product": product, "product": product,
"extra_launcher_options": options, "extra_launcher_options": options,
"write_results": write_results,
"subset": subset, "subset": subset,
} }
puppeteer = command_context._spawn(PuppeteerRunner) puppeteer = command_context._spawn(PuppeteerRunner)

File diff suppressed because it is too large Load diff

View file

@ -18,10 +18,10 @@ module.exports = {
reporter: 'dot', reporter: 'dot',
logLevel: 'debug', logLevel: 'debug',
require: ['./test/build/mocha-utils.js', 'source-map-support/register'], require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
spec: 'test/build/*.spec.js', spec: 'test/build/**/*.spec.js',
exit: !!process.env.CI, exit: !!process.env.CI,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
parallel: !!process.env.PARALLEL, parallel: !!process.env.PARALLEL,
timeout: 25 * 1000, timeout: 25_000,
reporter: process.env.CI ? 'spec' : 'dot', reporter: process.env.CI ? 'spec' : 'dot',
}; };

View file

@ -1,3 +1,3 @@
{ {
".": "17.1.2" ".": "18.0.0"
} }

View file

@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19)
### ⚠ BREAKING CHANGES
* fix bounding box visibility conditions (#8954)
### Features
* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1))
### Bug Fixes
* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53))
* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8))
* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7))
## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08)
### Bug Fixes
* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919)
* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915)
## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07) ## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)

View file

@ -15,10 +15,11 @@ to the project.
function JSONExtra(runner, options) { function JSONExtra(runner, options) {
mocha.reporters.Base.call(this, runner, options); mocha.reporters.Base.call(this, runner, options);
mocha.reporters.JSON.call(this, runner, options);
const self = this; const self = this;
runner.once(constants.EVENT_RUN_BEGIN, function () { runner.once(constants.EVENT_RUN_BEGIN, function () {
writeEvent(['start', { total: runner.total }]); writeEvent(['start', {total: runner.total}]);
}); });
runner.on(constants.EVENT_TEST_PASS, function (test) { runner.on(constants.EVENT_TEST_PASS, function (test) {

View file

@ -1,4 +1,3 @@
schema: 1
bugzilla: bugzilla:
component: Agent component: Agent
product: Remote Protocol product: Remote Protocol
@ -6,5 +5,6 @@ origin:
description: Headless Chrome Node API description: Headless Chrome Node API
license: Apache-2.0 license: Apache-2.0
name: puppeteer name: puppeteer
release: 0d2d99efeca73fba255fb10b28b5d3f50c2e20e4 release: 7d6927209e5d557891bd618ddb01d54bc3566307
url: https://github.com/puppeteer/puppeteer url: /Users/alexandraborovova/Projects/puppeteer
schema: 1

View file

@ -1,12 +1,12 @@
{ {
"name": "puppeteer", "name": "puppeteer",
"version": "17.1.2", "version": "18.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "puppeteer", "name": "puppeteer",
"version": "17.1.2", "version": "18.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -80,7 +80,8 @@
"text-diff": "1.0.1", "text-diff": "1.0.1",
"tsd": "0.22.0", "tsd": "0.22.0",
"tsx": "3.8.2", "tsx": "3.8.2",
"typescript": "4.7.4" "typescript": "4.7.4",
"zod": "3.18.0"
}, },
"engines": { "engines": {
"node": ">=14.1.0" "node": ">=14.1.0"
@ -7809,6 +7810,15 @@
"optionalDependencies": { "optionalDependencies": {
"commander": "^2.20.3" "commander": "^2.20.3"
} }
},
"node_modules/zod": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
}, },
"dependencies": { "dependencies": {
@ -13501,6 +13511,12 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"validator": "^13.7.0" "validator": "^13.7.0"
} }
},
"zod": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==",
"dev": true
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "puppeteer", "name": "puppeteer",
"version": "17.1.2", "version": "18.0.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol", "description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [ "keywords": [
"puppeteer", "puppeteer",
@ -27,14 +27,14 @@
"node": ">=14.1.0" "node": ">=14.1.0"
}, },
"scripts": { "scripts": {
"test": "c8 --check-coverage --lines 93 run-s test:chrome:* test:firefox", "test": "cross-env MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node utils/mochaRunner/lib/main.js",
"test:types": "tsd", "test:types": "tsd",
"test:install": "scripts/test-install.sh", "test:install": "scripts/test-install.sh",
"test:firefox": "cross-env PUPPETEER_PRODUCT=firefox MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", "test:firefox": "npm run test -- --test-suite firefox-headless",
"test:chrome": "run-s test:chrome:*", "test:chrome": "run-s test:chrome:*",
"test:chrome:headless": "cross-env HEADLESS=true PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", "test:chrome:headless": "npm run test -- --test-suite chrome-headless",
"test:chrome:headless-chrome": "cross-env HEADLESS=chrome PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", "test:chrome:headless-chrome": "npm run test -- --test-suite chrome-new-headless",
"test:chrome:headful": "cross-env HEADLESS=false PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", "test:chrome:headful": "npm run test -- --test-suite chrome-headful",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"prepare": "node typescript-if-required.js && husky install", "prepare": "node typescript-if-required.js && husky install",
"lint": "run-s lint:prettier lint:eslint", "lint": "run-s lint:prettier lint:eslint",
@ -139,6 +139,7 @@
"text-diff": "1.0.1", "text-diff": "1.0.1",
"tsd": "0.22.0", "tsd": "0.22.0",
"tsx": "3.8.2", "tsx": "3.8.2",
"typescript": "4.7.4" "typescript": "4.7.4",
"zod": "3.18.0"
} }
} }

View file

@ -127,3 +127,31 @@ echo '{"type":"module"}' >>$TMPDIR/package.json
npm install --loglevel silent "${tarball}" npm install --loglevel silent "${tarball}"
node --input-type="module" --eval="import puppeteer from 'puppeteer-core'" node --input-type="module" --eval="import puppeteer from 'puppeteer-core'"
node --input-type="module" --eval="import 'puppeteer-core/lib/esm/puppeteer/revisions.js';" node --input-type="module" --eval="import 'puppeteer-core/lib/esm/puppeteer/revisions.js';"
echo "Testing... Puppeteer Core launch with executablePath"
TMPDIR="$(mktemp -d)"
cd "$TMPDIR"
echo '{"type":"module"}' >> "$TMPDIR/package.json"
npm install --loglevel silent "${tarball}"
# The test tries to launch the node process because
# real browsers are not downloaded by puppeteer-core.
# The expected error is "Failed to launch the browser process"
# so the test verifies that it does not fail for other reasons.
node --input-type="module" --eval="
import puppeteer from 'puppeteer-core';
(async () => {
puppeteer.launch({
product: 'firefox',
executablePath: 'node'
}).catch(error => error.message.includes('Failed to launch the browser process') ? process.exit(0) : process.exit(1));
})();
"
node --input-type="module" --eval="
import puppeteer from 'puppeteer-core';
(async () => {
puppeteer.launch({
product: 'chrome',
executablePath: 'node'
}).catch(error => error.message.includes('Failed to launch the browser process') ? process.exit(0) : process.exit(1));
})();
"

View file

@ -0,0 +1,628 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Page} from '../common/Page.js'; // TODO: move to ./api
import type {Target} from '../common/Target.js'; // TODO: move to ./api
/**
* BrowserContext options.
*
* @public
*/
export interface BrowserContextOptions {
/**
* Proxy server with optional port to use for all requests.
* Username and password can be set in `Page.authenticate`.
*/
proxyServer?: string;
/**
* Bypass the proxy for the given list of hosts.
*/
proxyBypassList?: string[];
}
/**
* @internal
*/
export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
Permission,
Protocol.Browser.PermissionType
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
['persistent-storage', 'durableStorage'],
['idle-detection', 'idleDetection'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
/**
* @public
*/
export type Permission =
| 'geolocation'
| 'midi'
| 'notifications'
| 'camera'
| 'microphone'
| 'background-sync'
| 'ambient-light-sensor'
| 'accelerometer'
| 'gyroscope'
| 'magnetometer'
| 'accessibility-events'
| 'clipboard-read'
| 'clipboard-write'
| 'payment-handler'
| 'persistent-storage'
| 'idle-detection'
| 'midi-sysex';
/**
* @public
*/
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
* @defaultValue 30 seconds.
*/
timeout?: number;
}
/**
* All the events a {@link Browser | browser instance} may emit.
*
* @public
*/
export const enum BrowserEmittedEvents {
/**
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
* might happen because of one of the following:
*
* - Chromium is closed or crashed
*
* - The {@link Browser.disconnect | browser.disconnect } method was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the url of a target changes. Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target changes in incognito browser contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link Browser.newPage | browser.newPage}
*
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target creations in incognito browser contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target destructions in incognito browser contexts.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* A Browser is created when Puppeteer connects to a Chromium instance, either through
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
*
* @example
* An example of using a {@link Browser} to create a {@link Page}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* })();
* ```
*
* @example
* An example of disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to Chromium
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from Chromium
* browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close Chromium
* await browser2.close();
* })();
* ```
*
* @public
*/
export class Browser extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* @internal
*/
_attach(): Promise<void> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_detach(): void {
throw new Error('Not implemented');
}
/**
* @internal
*/
get _targets(): Map<string, Target> {
throw new Error('Not implemented');
}
/**
* The spawned browser process. Returns `null` if the browser instance was created with
* {@link Puppeteer.connect}.
*/
process(): ChildProcess | null {
throw new Error('Not implemented');
}
/**
* @internal
*/
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
throw new Error('Not implemented');
}
/**
* Creates a new incognito browser context. This won't share cookies/cache with other
* browser contexts.
*
* @example
*
* ```ts
* (async () => {
* const browser = await puppeteer.launch();
* // Create a new incognito browser context.
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* })();
* ```
*/
createIncognitoBrowserContext(
options?: BrowserContextOptions
): Promise<BrowserContext>;
createIncognitoBrowserContext(): Promise<BrowserContext> {
throw new Error('Not implemented');
}
/**
* Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}.
*/
browserContexts(): BrowserContext[] {
throw new Error('Not implemented');
}
/**
* Returns the default browser context. The default browser context cannot be closed.
*/
defaultBrowserContext(): BrowserContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
_disposeContext(contextId?: string): Promise<void>;
_disposeContext(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The browser websocket endpoint which can be used as an argument to
* {@link Puppeteer.connect}.
*
* @returns The Browser websocket url.
*
* @remarks
*
* The format is `ws://${host}:${port}/devtools/browser/<id>`.
*
* You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`.
* Learn more about the
* {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and
* the {@link
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}.
*/
wsEndpoint(): string {
throw new Error('Not implemented');
}
/**
* Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_createPageInContext(contextId?: string): Promise<Page>;
_createPageInContext(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* The target associated with the browser.
*/
target(): Target {
throw new Error('Not implemented');
}
/**
* Searches for a target in all browser contexts.
*
* @param predicate - A function to be run for every target.
* @returns The first target found that matches the `predicate` function.
*
* @example
*
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browser.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: WaitForTargetOptions
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all open pages inside the Browser.
*
* @remarks
*
* In case of multiple browser contexts, returns an array with all the pages in all
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
* here. You can find them using {@link Target.page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* A string representing the browser name and version.
*
* @remarks
*
* For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For
* non-headless, this is similar to `Chrome/61.0.3153.0`.
*
* The format of browser.version() might change with future releases of Chromium.
*/
version(): Promise<string> {
throw new Error('Not implemented');
}
/**
* The browser's original user agent. Pages can override the browser user agent with
* {@link Page.setUserAgent}.
*/
userAgent(): Promise<string> {
throw new Error('Not implemented');
}
/**
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object
* itself is considered to be disposed and cannot be used anymore.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Disconnects Puppeteer from the browser, but leaves the Chromium process running.
* After calling `disconnect`, the {@link Browser} object is considered disposed and
* cannot be used anymore.
*/
disconnect(): void {
throw new Error('Not implemented');
}
/**
* Indicates that the browser is connected.
*/
isConnected(): boolean {
throw new Error('Not implemented');
}
}
/**
* @public
*/
export const enum BrowserContextEmittedEvents {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* BrowserContexts provide a way to operate multiple independent browser
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/
export class BrowserContext extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* An array of all active targets inside the browser context.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* This searches for a target in this specific browser context.
*
* @example
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number}
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all pages inside the browser context.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
*
* @remarks
* The default browser context cannot be closed.
*/
isIncognito(): boolean {
throw new Error('Not implemented');
}
/**
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
*/
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
overridePermissions(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Clears all permission overrides for the browser context.
*
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
clearPermissionOverrides(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Creates a new page in the browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* The browser this browser context belongs to.
*/
browser(): Browser {
throw new Error('Not implemented');
}
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
*
* @remarks
* Only incognito browser contexts can be closed.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
}

View file

@ -19,8 +19,8 @@ import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
import {InternalQueryHandler} from './QueryHandler.js'; import {PuppeteerQueryHandler} from './QueryHandler.js';
async function queryAXTree( async function queryAXTree(
client: CDPSession, client: CDPSession,
@ -95,7 +95,7 @@ const queryOneId = async (element: ElementHandle<Node>, selector: string) => {
return res[0].backendDOMNodeId; return res[0].backendDOMNodeId;
}; };
const queryOne: InternalQueryHandler['queryOne'] = async ( const queryOne: PuppeteerQueryHandler['queryOne'] = async (
element, element,
selector selector
) => { ) => {
@ -108,7 +108,7 @@ const queryOne: InternalQueryHandler['queryOne'] = async (
)) as ElementHandle<Node>; )) as ElementHandle<Node>;
}; };
const waitFor: InternalQueryHandler['waitFor'] = async ( const waitFor: PuppeteerQueryHandler['waitFor'] = async (
elementOrFrame, elementOrFrame,
selector, selector,
options options
@ -121,21 +121,20 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
frame = elementOrFrame.frame; frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
} }
const binding: PageBinding = {
name: 'ariaQuerySelector', const ariaQuerySelector = async (selector: string) => {
pptrFunction: async (selector: string) => { const id = await queryOneId(
const id = await queryOneId( element || (await frame.worlds[PUPPETEER_WORLD].document()),
element || (await frame.worlds[PUPPETEER_WORLD].document()), selector
selector );
); if (!id) {
if (!id) { return null;
return null; }
} return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode( id
id )) as ElementHandle<Node>;
)) as ElementHandle<Node>;
},
}; };
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage( const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
(_: Element, selector: string) => { (_: Element, selector: string) => {
return ( return (
@ -147,22 +146,19 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
element, element,
selector, selector,
options, options,
binding new Set([ariaQuerySelector])
); );
if (element) { if (element) {
await element.dispose(); await element.dispose();
} }
if (!result) {
return null;
}
if (!(result instanceof ElementHandle)) { if (!(result instanceof ElementHandle)) {
await result.dispose(); await result?.dispose();
return null; return null;
} }
return result.frame.worlds[MAIN_WORLD].transferHandle(result); return result.frame.worlds[MAIN_WORLD].transferHandle(result);
}; };
const queryAll: InternalQueryHandler['queryAll'] = async ( const queryAll: PuppeteerQueryHandler['queryAll'] = async (
element, element,
selector selector
) => { ) => {
@ -182,7 +178,7 @@ const queryAll: InternalQueryHandler['queryAll'] = async (
/** /**
* @internal * @internal
*/ */
export const ariaHandler: InternalQueryHandler = { export const ariaHandler: PuppeteerQueryHandler = {
queryOne, queryOne,
waitFor, waitFor,
queryAll, queryAll,

View file

@ -18,7 +18,6 @@ import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {waitWithTimeout} from './util.js'; import {waitWithTimeout} from './util.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
@ -27,196 +26,24 @@ import {TaskQueue} from './TaskQueue.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js'; import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {ChromeTargetManager} from './ChromeTargetManager.js'; import {ChromeTargetManager} from './ChromeTargetManager.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js'; import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {
/** Browser as BrowserBase,
* BrowserContext options. BrowserContext,
* BrowserCloseCallback,
* @public TargetFilterCallback,
*/ IsPageTargetCallback,
export interface BrowserContextOptions { BrowserEmittedEvents,
/** BrowserContextEmittedEvents,
* Proxy server with optional port to use for all requests. BrowserContextOptions,
* Username and password can be set in `Page.authenticate`. WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
*/ WaitForTargetOptions,
proxyServer?: string;
/**
* Bypass the proxy for the given semi-colon-separated list of hosts.
*/
proxyBypassList?: string[];
}
/**
* @internal
*/
export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
Permission, Permission,
Protocol.Browser.PermissionType } from '../api/Browser.js';
>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
// TODO: push isn't a valid type?
// ['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardReadWrite'],
['payment-handler', 'paymentHandler'],
['persistent-storage', 'durableStorage'],
['idle-detection', 'idleDetection'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
/** /**
* @public * @internal
*/ */
export type Permission = export class CDPBrowser extends BrowserBase {
| 'geolocation'
| 'midi'
| 'notifications'
| 'camera'
| 'microphone'
| 'background-sync'
| 'ambient-light-sensor'
| 'accelerometer'
| 'gyroscope'
| 'magnetometer'
| 'accessibility-events'
| 'clipboard-read'
| 'clipboard-write'
| 'payment-handler'
| 'persistent-storage'
| 'idle-detection'
| 'midi-sysex';
/**
* @public
*/
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
* @defaultValue 30 seconds.
*/
timeout?: number;
}
/**
* All the events a {@link Browser | browser instance} may emit.
*
* @public
*/
export const enum BrowserEmittedEvents {
/**
* Emitted when Puppeteer gets disconnected from the Chromium instance. This
* might happen because of one of the following:
*
* - Chromium is closed or crashed
*
* - The {@link Browser.disconnect | browser.disconnect } method was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the url of a target changes. Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target changes in incognito browser contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link Browser.newPage | browser.newPage}
*
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target creations in incognito browser contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target destructions in incognito browser contexts.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* A Browser is created when Puppeteer connects to a Chromium instance, either through
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
*
* @example
* An example of using a {@link Browser} to create a {@link Page}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* })();
* ```
*
* @example
* An example of disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* const puppeteer = require('puppeteer');
*
* (async () => {
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to Chromium
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from Chromium
* browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close Chromium
* await browser2.close();
* })();
* ```
*
* @public
*/
export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
@ -230,8 +57,8 @@ export class Browser extends EventEmitter {
closeCallback?: BrowserCloseCallback, closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback, targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback isPageTargetCallback?: IsPageTargetCallback
): Promise<Browser> { ): Promise<CDPBrowser> {
const browser = new Browser( const browser = new CDPBrowser(
product, product,
connection, connection,
contextIds, contextIds,
@ -252,15 +79,15 @@ export class Browser extends EventEmitter {
#closeCallback: BrowserCloseCallback; #closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback; #targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback; #isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: BrowserContext; #defaultContext: CDPBrowserContext;
#contexts: Map<string, BrowserContext>; #contexts: Map<string, CDPBrowserContext>;
#screenshotTaskQueue: TaskQueue; #screenshotTaskQueue: TaskQueue;
#targetManager: TargetManager; #targetManager: TargetManager;
/** /**
* @internal * @internal
*/ */
get _targets(): Map<string, Target> { override get _targets(): Map<string, Target> {
return this.#targetManager.getAvailableTargets(); return this.#targetManager.getAvailableTargets();
} }
@ -305,12 +132,12 @@ export class Browser extends EventEmitter {
this.#targetFilterCallback this.#targetFilterCallback
); );
} }
this.#defaultContext = new BrowserContext(this.#connection, this); this.#defaultContext = new CDPBrowserContext(this.#connection, this);
this.#contexts = new Map(); this.#contexts = new Map();
for (const contextId of contextIds) { for (const contextId of contextIds) {
this.#contexts.set( this.#contexts.set(
contextId, contextId,
new BrowserContext(this.#connection, this, contextId) new CDPBrowserContext(this.#connection, this, contextId)
); );
} }
} }
@ -322,7 +149,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
async _attach(): Promise<void> { override async _attach(): Promise<void> {
this.#connection.on( this.#connection.on(
ConnectionEmittedEvents.Disconnected, ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected this.#emitDisconnected
@ -349,7 +176,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
_detach(): void { override _detach(): void {
this.#connection.off( this.#connection.off(
ConnectionEmittedEvents.Disconnected, ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected this.#emitDisconnected
@ -376,7 +203,7 @@ export class Browser extends EventEmitter {
* The spawned browser process. Returns `null` if the browser instance was created with * The spawned browser process. Returns `null` if the browser instance was created with
* {@link Puppeteer.connect}. * {@link Puppeteer.connect}.
*/ */
process(): ChildProcess | null { override process(): ChildProcess | null {
return this.#process ?? null; return this.#process ?? null;
} }
@ -402,7 +229,7 @@ export class Browser extends EventEmitter {
/** /**
* @internal * @internal
*/ */
_getIsPageTargetCallback(): IsPageTargetCallback | undefined { override _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
return this.#isPageTargetCallback; return this.#isPageTargetCallback;
} }
@ -424,9 +251,9 @@ export class Browser extends EventEmitter {
* })(); * })();
* ``` * ```
*/ */
async createIncognitoBrowserContext( override async createIncognitoBrowserContext(
options: BrowserContextOptions = {} options: BrowserContextOptions = {}
): Promise<BrowserContext> { ): Promise<CDPBrowserContext> {
const {proxyServer, proxyBypassList} = options; const {proxyServer, proxyBypassList} = options;
const {browserContextId} = await this.#connection.send( const {browserContextId} = await this.#connection.send(
@ -436,7 +263,7 @@ export class Browser extends EventEmitter {
proxyBypassList: proxyBypassList && proxyBypassList.join(','), proxyBypassList: proxyBypassList && proxyBypassList.join(','),
} }
); );
const context = new BrowserContext( const context = new CDPBrowserContext(
this.#connection, this.#connection,
this, this,
browserContextId browserContextId
@ -449,21 +276,21 @@ export class Browser extends EventEmitter {
* Returns an array of all open browser contexts. In a newly created browser, this will * Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}. * return a single instance of {@link BrowserContext}.
*/ */
browserContexts(): BrowserContext[] { override browserContexts(): CDPBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())]; return [this.#defaultContext, ...Array.from(this.#contexts.values())];
} }
/** /**
* Returns the default browser context. The default browser context cannot be closed. * Returns the default browser context. The default browser context cannot be closed.
*/ */
defaultBrowserContext(): BrowserContext { override defaultBrowserContext(): CDPBrowserContext {
return this.#defaultContext; return this.#defaultContext;
} }
/** /**
* @internal * @internal
*/ */
async _disposeContext(contextId?: string): Promise<void> { override async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) { if (!contextId) {
return; return;
} }
@ -564,7 +391,7 @@ export class Browser extends EventEmitter {
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}. * | browser endpoint}.
*/ */
wsEndpoint(): string { override wsEndpoint(): string {
return this.#connection.url(); return this.#connection.url();
} }
@ -572,14 +399,14 @@ export class Browser extends EventEmitter {
* Promise which resolves to a new {@link Page} object. The Page is created in * Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context. * a default browser context.
*/ */
async newPage(): Promise<Page> { override async newPage(): Promise<Page> {
return this.#defaultContext.newPage(); return this.#defaultContext.newPage();
} }
/** /**
* @internal * @internal
*/ */
async _createPageInContext(contextId?: string): Promise<Page> { override async _createPageInContext(contextId?: string): Promise<Page> {
const {targetId} = await this.#connection.send('Target.createTarget', { const {targetId} = await this.#connection.send('Target.createTarget', {
url: 'about:blank', url: 'about:blank',
browserContextId: contextId || undefined, browserContextId: contextId || undefined,
@ -605,7 +432,7 @@ export class Browser extends EventEmitter {
* All active targets inside the Browser. In case of multiple browser contexts, returns * All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts. * an array with all the targets in all browser contexts.
*/ */
targets(): Target[] { override targets(): Target[] {
return Array.from( return Array.from(
this.#targetManager.getAvailableTargets().values() this.#targetManager.getAvailableTargets().values()
).filter(target => { ).filter(target => {
@ -616,7 +443,7 @@ export class Browser extends EventEmitter {
/** /**
* The target associated with the browser. * The target associated with the browser.
*/ */
target(): Target { override target(): Target {
const browserTarget = this.targets().find(target => { const browserTarget = this.targets().find(target => {
return target.type() === 'browser'; return target.type() === 'browser';
}); });
@ -643,7 +470,7 @@ export class Browser extends EventEmitter {
* ); * );
* ``` * ```
*/ */
async waitForTarget( override async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
@ -683,7 +510,7 @@ export class Browser extends EventEmitter {
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed * browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
* here. You can find them using {@link Target.page}. * here. You can find them using {@link Target.page}.
*/ */
async pages(): Promise<Page[]> { override async pages(): Promise<Page[]> {
const contextPages = await Promise.all( const contextPages = await Promise.all(
this.browserContexts().map(context => { this.browserContexts().map(context => {
return context.pages(); return context.pages();
@ -705,7 +532,7 @@ export class Browser extends EventEmitter {
* *
* The format of browser.version() might change with future releases of Chromium. * The format of browser.version() might change with future releases of Chromium.
*/ */
async version(): Promise<string> { override async version(): Promise<string> {
const version = await this.#getVersion(); const version = await this.#getVersion();
return version.product; return version.product;
} }
@ -714,26 +541,27 @@ export class Browser extends EventEmitter {
* The browser's original user agent. Pages can override the browser user agent with * The browser's original user agent. Pages can override the browser user agent with
* {@link Page.setUserAgent}. * {@link Page.setUserAgent}.
*/ */
async userAgent(): Promise<string> { override async userAgent(): Promise<string> {
const version = await this.#getVersion(); const version = await this.#getVersion();
return version.userAgent; return version.userAgent;
} }
/** /**
* Closes Chromium and all of its pages (if any were opened). The {@link Browser} object * Closes Chromium and all of its pages (if any were opened). The
* itself is considered to be disposed and cannot be used anymore. * {@link CDPBrowser} object itself is considered to be disposed and cannot be
* used anymore.
*/ */
async close(): Promise<void> { override async close(): Promise<void> {
await this.#closeCallback.call(null); await this.#closeCallback.call(null);
this.disconnect(); this.disconnect();
} }
/** /**
* Disconnects Puppeteer from the browser, but leaves the Chromium process running. * Disconnects Puppeteer from the browser, but leaves the Chromium process running.
* After calling `disconnect`, the {@link Browser} object is considered disposed and * After calling `disconnect`, the {@link CDPBrowser} object is considered disposed and
* cannot be used anymore. * cannot be used anymore.
*/ */
disconnect(): void { override disconnect(): void {
this.#targetManager.dispose(); this.#targetManager.dispose();
this.#connection.dispose(); this.#connection.dispose();
} }
@ -741,7 +569,7 @@ export class Browser extends EventEmitter {
/** /**
* Indicates that the browser is connected. * Indicates that the browser is connected.
*/ */
isConnected(): boolean { override isConnected(): boolean {
return !this.#connection._closed; return !this.#connection._closed;
} }
@ -749,75 +577,19 @@ export class Browser extends EventEmitter {
return this.#connection.send('Browser.getVersion'); return this.#connection.send('Browser.getVersion');
} }
} }
/**
* @public
*/
export const enum BrowserContextEmittedEvents {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
/** /**
* BrowserContexts provide a way to operate multiple independent browser * @internal
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/ */
export class BrowserContext extends EventEmitter { export class CDPBrowserContext extends BrowserContext {
#connection: Connection; #connection: Connection;
#browser: Browser; #browser: CDPBrowser;
#id?: string; #id?: string;
/** /**
* @internal * @internal
*/ */
constructor(connection: Connection, browser: Browser, contextId?: string) { constructor(connection: Connection, browser: CDPBrowser, contextId?: string) {
super(); super();
this.#connection = connection; this.#connection = connection;
this.#browser = browser; this.#browser = browser;
@ -827,7 +599,7 @@ export class BrowserContext extends EventEmitter {
/** /**
* An array of all active targets inside the browser context. * An array of all active targets inside the browser context.
*/ */
targets(): Target[] { override targets(): Target[] {
return this.#browser.targets().filter(target => { return this.#browser.targets().filter(target => {
return target.browserContext() === this; return target.browserContext() === this;
}); });
@ -853,7 +625,7 @@ export class BrowserContext extends EventEmitter {
* @returns Promise which resolves to the first target found * @returns Promise which resolves to the first target found
* that matches the `predicate` function. * that matches the `predicate` function.
*/ */
waitForTarget( override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {} options: {timeout?: number} = {}
): Promise<Target> { ): Promise<Target> {
@ -869,7 +641,7 @@ export class BrowserContext extends EventEmitter {
* Non visible pages, such as `"background_page"`, will not be listed here. * Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}. * You can find them using {@link Target.page | the target page}.
*/ */
async pages(): Promise<Page[]> { override async pages(): Promise<Page[]> {
const pages = await Promise.all( const pages = await Promise.all(
this.targets() this.targets()
.filter(target => { .filter(target => {
@ -897,7 +669,7 @@ export class BrowserContext extends EventEmitter {
* @remarks * @remarks
* The default browser context cannot be closed. * The default browser context cannot be closed.
*/ */
isIncognito(): boolean { override isIncognito(): boolean {
return !!this.#id; return !!this.#id;
} }
@ -915,7 +687,7 @@ export class BrowserContext extends EventEmitter {
* @param permissions - An array of permissions to grant. * @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied. * All permissions that are not listed here will be automatically denied.
*/ */
async overridePermissions( override async overridePermissions(
origin: string, origin: string,
permissions: Permission[] permissions: Permission[]
): Promise<void> { ): Promise<void> {
@ -946,7 +718,7 @@ export class BrowserContext extends EventEmitter {
* context.clearPermissionOverrides(); * context.clearPermissionOverrides();
* ``` * ```
*/ */
async clearPermissionOverrides(): Promise<void> { override async clearPermissionOverrides(): Promise<void> {
await this.#connection.send('Browser.resetPermissions', { await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined, browserContextId: this.#id || undefined,
}); });
@ -955,14 +727,14 @@ export class BrowserContext extends EventEmitter {
/** /**
* Creates a new page in the browser context. * Creates a new page in the browser context.
*/ */
newPage(): Promise<Page> { override newPage(): Promise<Page> {
return this.#browser._createPageInContext(this.#id); return this.#browser._createPageInContext(this.#id);
} }
/** /**
* The browser this browser context belongs to. * The browser this browser context belongs to.
*/ */
browser(): Browser { override browser(): CDPBrowser {
return this.#browser; return this.#browser;
} }
@ -973,7 +745,7 @@ export class BrowserContext extends EventEmitter {
* @remarks * @remarks
* Only incognito browser contexts can be closed. * Only incognito browser contexts can be closed.
*/ */
async close(): Promise<void> { override async close(): Promise<void> {
assert(this.#id, 'Non-incognito profiles cannot be closed!'); assert(this.#id, 'Non-incognito profiles cannot be closed!');
await this.#browser._disposeContext(this.#id); await this.#browser._disposeContext(this.#id);
} }

View file

@ -18,11 +18,8 @@ import {debugError} from './util.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import { import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
Browser, import {CDPBrowser} from './Browser.js';
IsPageTargetCallback,
TargetFilterCallback,
} from './Browser.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {ConnectionTransport} from './ConnectionTransport.js'; import {ConnectionTransport} from './ConnectionTransport.js';
import {getFetch} from './fetch.js'; import {getFetch} from './fetch.js';
@ -55,6 +52,11 @@ export interface BrowserConnectOptions {
* @internal * @internal
*/ */
_isPageTarget?: IsPageTargetCallback; _isPageTarget?: IsPageTargetCallback;
/**
* @defaultValue 'cdp'
* @internal
*/
protocol?: 'cdp' | 'webDriverBiDi';
} }
const getWebSocketTransportClass = async () => { const getWebSocketTransportClass = async () => {
@ -70,13 +72,13 @@ const getWebSocketTransportClass = async () => {
* *
* @internal * @internal
*/ */
export async function _connectToBrowser( export async function _connectToCDPBrowser(
options: BrowserConnectOptions & { options: BrowserConnectOptions & {
browserWSEndpoint?: string; browserWSEndpoint?: string;
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
} }
): Promise<Browser> { ): Promise<CDPBrowser> {
const { const {
browserWSEndpoint, browserWSEndpoint,
browserURL, browserURL,
@ -118,7 +120,7 @@ export async function _connectToBrowser(
const {browserContextIds} = await connection.send( const {browserContextIds} = await connection.send(
'Target.getBrowserContexts' 'Target.getBrowserContexts'
); );
const browser = await Browser._create( const browser = await CDPBrowser._create(
product || 'chrome', product || 'chrome',
connection, connection,
browserContextIds, browserContextIds,
@ -131,7 +133,6 @@ export async function _connectToBrowser(
targetFilter, targetFilter,
isPageTarget isPageTarget
); );
await browser.pages();
return browser; return browser;
} }

View file

@ -20,7 +20,7 @@ import {CDPSession, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
import {TargetFilterCallback} from './Browser.js'; import {TargetFilterCallback} from '../api/Browser.js';
import { import {
TargetInterceptor, TargetInterceptor,
TargetFactory, TargetFactory,

View file

@ -56,7 +56,7 @@ export class Connection extends EventEmitter {
#transport: ConnectionTransport; #transport: ConnectionTransport;
#delay: number; #delay: number;
#lastId = 0; #lastId = 0;
#sessions: Map<string, CDPSession> = new Map(); #sessions: Map<string, CDPSessionImpl> = new Map();
#closed = false; #closed = false;
#callbacks: Map<number, ConnectionCallback> = new Map(); #callbacks: Map<number, ConnectionCallback> = new Map();
#manuallyAttached = new Set<string>(); #manuallyAttached = new Set<string>();
@ -147,7 +147,7 @@ export class Connection extends EventEmitter {
const object = JSON.parse(message); const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') { if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId; const sessionId = object.params.sessionId;
const session = new CDPSession( const session = new CDPSessionImpl(
this, this,
object.params.targetInfo.type, object.params.targetInfo.type,
sessionId sessionId
@ -310,6 +310,47 @@ export const CDPSessionEmittedEvents = {
* @public * @public
*/ */
export class CDPSession extends EventEmitter { export class CDPSession extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
connection(): Connection | undefined {
throw new Error('Not implemented');
}
send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']>;
send<T extends keyof ProtocolMapping.Commands>(): Promise<
ProtocolMapping.Commands[T]['returnType']
> {
throw new Error('Not implemented');
}
/**
* Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages.
*/
async detach(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Returns the session's id.
*/
id(): string {
throw new Error('Not implemented');
}
}
/**
* @internal
*/
export class CDPSessionImpl extends CDPSession {
#sessionId: string; #sessionId: string;
#targetType: string; #targetType: string;
#callbacks: Map<number, ConnectionCallback> = new Map(); #callbacks: Map<number, ConnectionCallback> = new Map();
@ -325,11 +366,11 @@ export class CDPSession extends EventEmitter {
this.#sessionId = sessionId; this.#sessionId = sessionId;
} }
connection(): Connection | undefined { override connection(): Connection | undefined {
return this.#connection; return this.#connection;
} }
send<T extends keyof ProtocolMapping.Commands>( override send<T extends keyof ProtocolMapping.Commands>(
method: T, method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType'] ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> { ): Promise<ProtocolMapping.Commands[T]['returnType']> {
@ -386,7 +427,7 @@ export class CDPSession extends EventEmitter {
* Detaches the cdpSession from the target. Once detached, the cdpSession object * Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages. * won't emit any events and can't be used to send messages.
*/ */
async detach(): Promise<void> { override async detach(): Promise<void> {
if (!this.#connection) { if (!this.#connection) {
throw new Error( throw new Error(
`Session already detached. Most likely the ${ `Session already detached. Most likely the ${
@ -419,7 +460,7 @@ export class CDPSession extends EventEmitter {
/** /**
* Returns the session's id. * Returns the session's id.
*/ */
id(): string { override id(): string {
return this.#sessionId; return this.#sessionId;
} }
} }
@ -445,3 +486,13 @@ function rewriteError(
error.originalMessage = originalMessage ?? error.originalMessage; error.originalMessage = originalMessage ?? error.originalMessage;
return error; return error;
} }
/**
* @internal
*/
export function isTargetClosedError(err: Error): boolean {
return (
err.message.includes('Target closed') ||
err.message.includes('Session closed')
);
}

View file

@ -142,7 +142,8 @@ export class Coverage {
* Anonymous scripts are ones that don't have an associated url. These are * Anonymous scripts are ones that don't have an associated url. These are
* scripts that are dynamically created on the page using `eval` or * scripts that are dynamically created on the page using `eval` or
* `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous * `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
* scripts will have `pptr://__puppeteer_evaluation_script__` as their URL. * scripts URL will start with `debugger://VM` (unless a magic //# sourceURL
* comment is present, in which case that will the be URL).
*/ */
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
return await this.#jsCoverage.start(options); return await this.#jsCoverage.start(options);

View file

@ -1,3 +1,19 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';

View file

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

View file

@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {EvaluateFunc, HandleFor} from './types.js'; import {EvaluateFunc, HandleFor} from './types.js';
import { import {
createJSHandle, createJSHandle,
@ -273,7 +274,7 @@ export class ExecutionContext {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n', functionDeclaration: functionText + '\n' + suffix + '\n',
executionContextId: this._contextId, executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)), arguments: await Promise.all(args.map(convertArgument.bind(this))),
returnByValue, returnByValue,
awaitPromise: true, awaitPromise: true,
userGesture: true, userGesture: true,
@ -298,10 +299,13 @@ export class ExecutionContext {
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : createJSHandle(this, remoteObject);
function convertArgument( async function convertArgument(
this: ExecutionContext, this: ExecutionContext,
arg: unknown arg: unknown
): Protocol.Runtime.CallArgument { ): Promise<Protocol.Runtime.CallArgument> {
if (arg instanceof LazyArg) {
arg = await arg.get();
}
if (typeof arg === 'bigint') { if (typeof arg === 'bigint') {
// eslint-disable-line valid-typeof // eslint-disable-line valid-typeof
return {unserializableValue: `${arg.toString()}n`}; return {unserializableValue: `${arg.toString()}n`};

View file

@ -18,7 +18,7 @@ import Protocol from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js'; import {CDPSession, Connection} from './Connection.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {TargetFilterCallback} from './Browser.js'; import {TargetFilterCallback} from '../api/Browser.js';
import { import {
TargetFactory, TargetFactory,
TargetInterceptor, TargetInterceptor,

View file

@ -1,3 +1,19 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
@ -36,7 +52,7 @@ export interface FrameWaitForFunctionOptions {
* *
* - `mutation` - to execute `pageFunction` on every DOM mutation. * - `mutation` - to execute `pageFunction` on every DOM mutation.
*/ */
polling?: string | number; polling?: 'raf' | 'mutation' | number;
/** /**
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds). * Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed * Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
@ -150,7 +166,6 @@ export interface FrameAddStyleTagOptions {
* @public * @public
*/ */
export class Frame { export class Frame {
#parentFrame: Frame | null;
#url = ''; #url = '';
#detached = false; #detached = false;
#client!: CDPSession; #client!: CDPSession;
@ -186,30 +201,25 @@ export class Frame {
/** /**
* @internal * @internal
*/ */
_childFrames: Set<Frame>; _parentId?: string;
/** /**
* @internal * @internal
*/ */
constructor( constructor(
frameManager: FrameManager, frameManager: FrameManager,
parentFrame: Frame | null,
frameId: string, frameId: string,
parentFrameId: string | undefined,
client: CDPSession client: CDPSession
) { ) {
this._frameManager = frameManager; this._frameManager = frameManager;
this.#parentFrame = parentFrame ?? null;
this.#url = ''; this.#url = '';
this._id = frameId; this._id = frameId;
this._parentId = parentFrameId;
this.#detached = false; this.#detached = false;
this._loaderId = ''; this._loaderId = '';
this._childFrames = new Set();
if (this.#parentFrame) {
this.#parentFrame._childFrames.add(this);
}
this.updateClient(client); this.updateClient(client);
} }
@ -220,7 +230,7 @@ export class Frame {
this.#client = client; this.#client = client;
this.worlds = { this.worlds = {
[MAIN_WORLD]: new IsolatedWorld(this), [MAIN_WORLD]: new IsolatedWorld(this),
[PUPPETEER_WORLD]: new IsolatedWorld(this, true), [PUPPETEER_WORLD]: new IsolatedWorld(this),
}; };
} }
@ -664,7 +674,6 @@ export class Frame {
options: FrameWaitForFunctionOptions = {}, options: FrameWaitForFunctionOptions = {},
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
// TODO: Fix when NodeHandle has been added.
return this.worlds[MAIN_WORLD].waitForFunction( return this.worlds[MAIN_WORLD].waitForFunction(
pageFunction, pageFunction,
options, options,
@ -721,14 +730,14 @@ export class Frame {
* @returns The parent frame, if any. Detached and main frames return `null`. * @returns The parent frame, if any. Detached and main frames return `null`.
*/ */
parentFrame(): Frame | null { parentFrame(): Frame | null {
return this.#parentFrame; return this._frameManager._frameTree.parentFrame(this._id) || null;
} }
/** /**
* @returns An array of child frames. * @returns An array of child frames.
*/ */
childFrames(): Frame[] { childFrames(): Frame[] {
return Array.from(this._childFrames); return this._frameManager._frameTree.childFrames(this._id);
} }
/** /**
@ -776,8 +785,8 @@ export class Frame {
return this.worlds[MAIN_WORLD].transferHandle( return this.worlds[MAIN_WORLD].transferHandle(
await this.worlds[PUPPETEER_WORLD].evaluateHandle( await this.worlds[PUPPETEER_WORLD].evaluateHandle(
async ({url, id, type, content}) => { async ({createDeferredPromise}, {url, id, type, content}) => {
const promise = InjectedUtil.createDeferredPromise<void>(); const promise = createDeferredPromise<void>();
const script = document.createElement('script'); const script = document.createElement('script');
script.type = type; script.type = type;
script.text = content; script.text = content;
@ -809,6 +818,7 @@ export class Frame {
await promise; await promise;
return script; return script;
}, },
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
{...options, type, content} {...options, type, content}
) )
); );
@ -858,8 +868,8 @@ export class Frame {
return this.worlds[MAIN_WORLD].transferHandle( return this.worlds[MAIN_WORLD].transferHandle(
await this.worlds[PUPPETEER_WORLD].evaluateHandle( await this.worlds[PUPPETEER_WORLD].evaluateHandle(
async ({url, content}) => { async ({createDeferredPromise}, {url, content}) => {
const promise = InjectedUtil.createDeferredPromise<void>(); const promise = createDeferredPromise<void>();
let element: HTMLStyleElement | HTMLLinkElement; let element: HTMLStyleElement | HTMLLinkElement;
if (!url) { if (!url) {
element = document.createElement('style'); element = document.createElement('style');
@ -892,6 +902,7 @@ export class Frame {
await promise; await promise;
return element; return element;
}, },
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
options options
) )
); );
@ -1089,9 +1100,5 @@ export class Frame {
this.#detached = true; this.#detached = true;
this.worlds[MAIN_WORLD]._detach(); this.worlds[MAIN_WORLD]._detach();
this.worlds[PUPPETEER_WORLD]._detach(); this.worlds[PUPPETEER_WORLD]._detach();
if (this.#parentFrame) {
this.#parentFrame._childFrames.delete(this);
}
this.#parentFrame = null;
} }
} }

View file

@ -16,13 +16,12 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
import {DeferredPromise} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {CDPSession, isTargetClosedError} from './Connection.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {FrameTree} from './FrameTree.js';
import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js'; import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
import {NetworkManager} from './NetworkManager.js'; import {NetworkManager} from './NetworkManager.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
@ -60,20 +59,13 @@ export class FrameManager extends EventEmitter {
#page: Page; #page: Page;
#networkManager: NetworkManager; #networkManager: NetworkManager;
#timeoutSettings: TimeoutSettings; #timeoutSettings: TimeoutSettings;
#frames = new Map<string, Frame>();
#contextIdToContext = new Map<string, ExecutionContext>(); #contextIdToContext = new Map<string, ExecutionContext>();
#isolatedWorlds = new Set<string>(); #isolatedWorlds = new Set<string>();
#mainFrame?: Frame;
#client: CDPSession; #client: CDPSession;
/** /**
* Keeps track of OOPIF targets/frames (target ID == frame ID for OOPIFs) * @internal
* that are being initialized.
*/ */
#framesPendingTargetInit = new Map<string, DeferredPromise<void>>(); _frameTree = new FrameTree();
/**
* Keeps track of frames that are in the process of being attached in #onFrameAttached.
*/
#framesPendingAttachment = new Map<string, DeferredPromise<void>>();
get timeoutSettings(): TimeoutSettings { get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings; return this.#timeoutSettings;
@ -140,19 +132,8 @@ export class FrameManager extends EventEmitter {
}); });
} }
async initialize( async initialize(client: CDPSession = this.#client): Promise<void> {
targetId: string,
client: CDPSession = this.#client
): Promise<void> {
try { try {
if (!this.#framesPendingTargetInit.has(targetId)) {
this.#framesPendingTargetInit.set(
targetId,
createDebuggableDeferredPromise(
`Waiting for target frame ${targetId} failed`
)
);
}
const result = await Promise.all([ const result = await Promise.all([
client.send('Page.enable'), client.send('Page.enable'),
client.send('Page.getFrameTree'), client.send('Page.getFrameTree'),
@ -172,18 +153,11 @@ export class FrameManager extends EventEmitter {
]); ]);
} catch (error) { } catch (error) {
// The target might have been closed before the initialization finished. // The target might have been closed before the initialization finished.
if ( if (isErrorLike(error) && isTargetClosedError(error)) {
isErrorLike(error) &&
(error.message.includes('Target closed') ||
error.message.includes('Session closed'))
) {
return; return;
} }
throw error; throw error;
} finally {
this.#framesPendingTargetInit.get(targetId)?.resolve();
this.#framesPendingTargetInit.delete(targetId);
} }
} }
@ -202,16 +176,17 @@ export class FrameManager extends EventEmitter {
} }
mainFrame(): Frame { mainFrame(): Frame {
assert(this.#mainFrame, 'Requesting main frame too early!'); const mainFrame = this._frameTree.getMainFrame();
return this.#mainFrame; assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
} }
frames(): Frame[] { frames(): Frame[] {
return Array.from(this.#frames.values()); return Array.from(this._frameTree.frames());
} }
frame(frameId: string): Frame | null { frame(frameId: string): Frame | null {
return this.#frames.get(frameId) || null; return this._frameTree.getById(frameId) || null;
} }
onAttachedToTarget(target: Target): void { onAttachedToTarget(target: Target): void {
@ -219,16 +194,16 @@ export class FrameManager extends EventEmitter {
return; return;
} }
const frame = this.#frames.get(target._getTargetInfo().targetId); const frame = this.frame(target._getTargetInfo().targetId);
if (frame) { if (frame) {
frame.updateClient(target._session()!); frame.updateClient(target._session()!);
} }
this.setupEventListeners(target._session()!); this.setupEventListeners(target._session()!);
this.initialize(target._getTargetInfo().targetId, target._session()); this.initialize(target._session());
} }
onDetachedFromTarget(target: Target): void { onDetachedFromTarget(target: Target): void {
const frame = this.#frames.get(target._targetId); const frame = this.frame(target._targetId);
if (frame && frame.isOOPFrame()) { if (frame && frame.isOOPFrame()) {
// When an OOP iframe is removed from the page, it // When an OOP iframe is removed from the page, it
// will only get a Target.detachedFromTarget event. // will only get a Target.detachedFromTarget event.
@ -237,7 +212,7 @@ export class FrameManager extends EventEmitter {
} }
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.#frames.get(event.frameId); const frame = this.frame(event.frameId);
if (!frame) { if (!frame) {
return; return;
} }
@ -246,7 +221,7 @@ export class FrameManager extends EventEmitter {
} }
#onFrameStartedLoading(frameId: string): void { #onFrameStartedLoading(frameId: string): void {
const frame = this.#frames.get(frameId); const frame = this.frame(frameId);
if (!frame) { if (!frame) {
return; return;
} }
@ -254,7 +229,7 @@ export class FrameManager extends EventEmitter {
} }
#onFrameStoppedLoading(frameId: string): void { #onFrameStoppedLoading(frameId: string): void {
const frame = this.#frames.get(frameId); const frame = this.frame(frameId);
if (!frame) { if (!frame) {
return; return;
} }
@ -288,8 +263,8 @@ export class FrameManager extends EventEmitter {
frameId: string, frameId: string,
parentFrameId: string parentFrameId: string
): void { ): void {
if (this.#frames.has(frameId)) { let frame = this.frame(frameId);
const frame = this.#frames.get(frameId)!; if (frame) {
if (session && frame.isOOPFrame()) { if (session && frame.isOOPFrame()) {
// If an OOP iframes becomes a normal iframe again // If an OOP iframes becomes a normal iframe again
// it is first attached to the parent page before // it is first attached to the parent page before
@ -298,86 +273,41 @@ export class FrameManager extends EventEmitter {
} }
return; return;
} }
const parentFrame = this.#frames.get(parentFrameId);
const complete = (parentFrame: Frame) => { frame = new Frame(this, frameId, parentFrameId, session);
assert(parentFrame, `Parent frame ${parentFrameId} not found`); this._frameTree.addFrame(frame);
const frame = new Frame(this, parentFrame, frameId, session); this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
this.#frames.set(frame._id, frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
};
if (parentFrame) {
return complete(parentFrame);
}
const frame = this.#framesPendingTargetInit.get(parentFrameId);
if (frame) {
if (!this.#framesPendingAttachment.has(frameId)) {
this.#framesPendingAttachment.set(
frameId,
createDebuggableDeferredPromise(
`Waiting for frame ${frameId} to attach failed`
)
);
}
frame.then(() => {
complete(this.#frames.get(parentFrameId)!);
this.#framesPendingAttachment.get(frameId)?.resolve();
this.#framesPendingAttachment.delete(frameId);
});
return;
}
throw new Error(`Parent frame ${parentFrameId} not found`);
} }
#onFrameNavigated(framePayload: Protocol.Page.Frame): void { async #onFrameNavigated(framePayload: Protocol.Page.Frame): Promise<void> {
const frameId = framePayload.id; const frameId = framePayload.id;
const isMainFrame = !framePayload.parentId; const isMainFrame = !framePayload.parentId;
const frame = isMainFrame ? this.#mainFrame : this.#frames.get(frameId);
const complete = (frame?: Frame) => { let frame = this._frameTree.getById(frameId);
assert(
isMainFrame || frame,
`Missing frame isMainFrame=${isMainFrame}, frameId=${frameId}`
);
// Detach all child frames first. // Detach all child frames first.
if (frame) { if (frame) {
for (const child of frame.childFrames()) { for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child); this.#removeFramesRecursively(child);
}
} }
// Update or create main frame.
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this.#frames.delete(frame._id);
frame._id = frameId;
} else {
// Initial main frame navigation.
frame = new Frame(this, null, frameId, this.#client);
}
this.#frames.set(frameId, frame);
this.#mainFrame = frame;
}
// Update frame payload.
assert(frame);
frame._navigated(framePayload);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
};
const pendingFrame = this.#framesPendingAttachment.get(frameId);
if (pendingFrame) {
pendingFrame.then(() => {
complete(isMainFrame ? this.#mainFrame : this.#frames.get(frameId));
});
} else {
complete(frame);
} }
// Update or create main frame.
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this._frameTree.removeFrame(frame);
frame._id = frameId;
} else {
// Initial main frame navigation.
frame = new Frame(this, frameId, undefined, this.#client);
}
this._frameTree.addFrame(frame);
}
frame = await this._frameTree.waitForFrame(frameId);
frame._navigated(framePayload);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
} }
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> { async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
@ -414,7 +344,7 @@ export class FrameManager extends EventEmitter {
} }
#onFrameNavigatedWithinDocument(frameId: string, url: string): void { #onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this.#frames.get(frameId); const frame = this.frame(frameId);
if (!frame) { if (!frame) {
return; return;
} }
@ -427,7 +357,7 @@ export class FrameManager extends EventEmitter {
frameId: string, frameId: string,
reason: Protocol.Page.FrameDetachedEventReason reason: Protocol.Page.FrameDetachedEventReason
): void { ): void {
const frame = this.#frames.get(frameId); const frame = this.frame(frameId);
if (reason === 'remove') { if (reason === 'remove') {
// Only remove the frame if the reason for the detached event is // Only remove the frame if the reason for the detached event is
// an actual removement of the frame. // an actual removement of the frame.
@ -446,8 +376,7 @@ export class FrameManager extends EventEmitter {
): void { ): void {
const auxData = contextPayload.auxData as {frameId?: string} | undefined; const auxData = contextPayload.auxData as {frameId?: string} | undefined;
const frameId = auxData && auxData.frameId; const frameId = auxData && auxData.frameId;
const frame = const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
typeof frameId === 'string' ? this.#frames.get(frameId) : undefined;
let world: IsolatedWorld | undefined; let world: IsolatedWorld | undefined;
if (frame) { if (frame) {
// Only care about execution contexts created for the current session. // Only care about execution contexts created for the current session.
@ -513,7 +442,7 @@ export class FrameManager extends EventEmitter {
this.#removeFramesRecursively(child); this.#removeFramesRecursively(child);
} }
frame._detach(); frame._detach();
this.#frames.delete(frame._id); this._frameTree.removeFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameDetached, frame); this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
} }
} }

View file

@ -0,0 +1,111 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createDeferredPromise,
DeferredPromise,
} from '../util/DeferredPromise.js';
import type {Frame} from './Frame.js';
/**
* Keeps track of the page frame tree and it's is managed by
* {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
* means that referenced frames might not be in the tree anymore. Thus, the tree
* structure is eventually consistent.
* @internal
*/
export class FrameTree {
#frames = new Map<string, Frame>();
// frameID -> parentFrameID
#parentIds = new Map<string, string>();
// frameID -> childFrameIDs
#childIds = new Map<string, Set<string>>();
#mainFrame?: Frame;
#waitRequests = new Map<string, Set<DeferredPromise<Frame>>>();
getMainFrame(): Frame | undefined {
return this.#mainFrame;
}
getById(frameId: string): Frame | undefined {
return this.#frames.get(frameId);
}
/**
* Returns a promise that is resolved once the frame with
* the given ID is added to the tree.
*/
waitForFrame(frameId: string): Promise<Frame> {
const frame = this.getById(frameId);
if (frame) {
return Promise.resolve(frame);
}
const deferred = createDeferredPromise<Frame>();
const callbacks =
this.#waitRequests.get(frameId) || new Set<DeferredPromise<Frame>>();
callbacks.add(deferred);
return deferred;
}
frames(): Frame[] {
return Array.from(this.#frames.values());
}
addFrame(frame: Frame): void {
this.#frames.set(frame._id, frame);
if (frame._parentId) {
this.#parentIds.set(frame._id, frame._parentId);
if (!this.#childIds.has(frame._parentId)) {
this.#childIds.set(frame._parentId, new Set());
}
this.#childIds.get(frame._parentId)!.add(frame._id);
} else {
this.#mainFrame = frame;
}
this.#waitRequests.get(frame._id)?.forEach(request => {
return request.resolve(frame);
});
}
removeFrame(frame: Frame): void {
this.#frames.delete(frame._id);
this.#parentIds.delete(frame._id);
if (frame._parentId) {
this.#childIds.get(frame._parentId)?.delete(frame._id);
} else {
this.#mainFrame = undefined;
}
}
childFrames(frameId: string): Frame[] {
const childIds = this.#childIds.get(frameId);
if (!childIds) {
return [];
}
return Array.from(childIds)
.map(id => {
return this.getById(id);
})
.filter((frame): frame is Frame => {
return frame !== undefined;
});
}
parentFrame(frameId: string): Frame | undefined {
const parentId = this.#parentIds.get(frameId);
return parentId ? this.getById(parentId) : undefined;
}
}

View file

@ -16,39 +16,23 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {source as injectedSource} from '../generated/injected.js'; import {source as injectedSource} from '../generated/injected.js';
import type PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {createDeferredPromise} from '../util/DeferredPromise.js'; import {createDeferredPromise} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {TimeoutError} from './Errors.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js'; import {MouseButton} from './Input.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import { import {createJSHandle, debugError, pageBindingInitString} from './util.js';
createJSHandle, import {TaskManager, WaitTask} from './WaitTask.js';
debugError,
isNumber,
isString,
makePredicateString,
pageBindingInitString,
} from './util.js';
// predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below.
declare const predicateQueryHandler: (
element: Element | Document,
selector: string
) => Promise<Element | Element[] | NodeListOf<Element>>;
declare const checkWaitForOptions: (
node: Node | null,
waitForVisible: boolean,
waitForHidden: boolean
) => Element | null | boolean;
/** /**
* @public * @public
@ -114,7 +98,6 @@ export interface IsolatedWorldChart {
*/ */
export class IsolatedWorld { export class IsolatedWorld {
#frame: Frame; #frame: Frame;
#injected: boolean;
#document?: ElementHandle<Document>; #document?: ElementHandle<Document>;
#context = createDeferredPromise<ExecutionContext>(); #context = createDeferredPromise<ExecutionContext>();
#detached = false; #detached = false;
@ -124,10 +107,15 @@ export class IsolatedWorld {
// Contains mapping from functions that should be bound to Puppeteer functions. // Contains mapping from functions that should be bound to Puppeteer functions.
#boundFunctions = new Map<string, Function>(); #boundFunctions = new Map<string, Function>();
#waitTasks = new Set<WaitTask>(); #taskManager = new TaskManager();
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
get _waitTasks(): Set<WaitTask> { get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
return this.#waitTasks; return this.#puppeteerUtil;
}
get taskManager(): TaskManager {
return this.#taskManager;
} }
get _boundFunctions(): Map<string, Function> { get _boundFunctions(): Map<string, Function> {
@ -138,11 +126,10 @@ export class IsolatedWorld {
return `${name}_${contextId}`; return `${name}_${contextId}`;
}; };
constructor(frame: Frame, injected = false) { constructor(frame: Frame) {
// Keep own reference to client because it might differ from the FrameManager's // Keep own reference to client because it might differ from the FrameManager's
// client for OOP iframes. // client for OOP iframes.
this.#frame = frame; this.#frame = frame;
this.#injected = injected;
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled); this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
} }
@ -164,17 +151,30 @@ export class IsolatedWorld {
clearContext(): void { clearContext(): void {
this.#document = undefined; this.#document = undefined;
this.#puppeteerUtil = createDeferredPromise();
this.#context = createDeferredPromise(); this.#context = createDeferredPromise();
} }
setContext(context: ExecutionContext): void { setContext(context: ExecutionContext): void {
if (this.#injected) { this.#injectPuppeteerUtil(context);
context.evaluate(injectedSource).catch(debugError);
}
this.#ctxBindings.clear(); this.#ctxBindings.clear();
this.#context.resolve(context); this.#context.resolve(context);
for (const waitTask of this._waitTasks) { }
waitTask.rerun();
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
try {
this.#puppeteerUtil.resolve(
(await context.evaluateHandle(
`(() => {
const module = {};
${injectedSource}
return module.exports.default;
})()`
)) as JSHandle<PuppeteerUtil>
);
this.#taskManager.rerunAll();
} catch (error: unknown) {
debugError(error);
} }
} }
@ -185,11 +185,9 @@ export class IsolatedWorld {
_detach(): void { _detach(): void {
this.#detached = true; this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled); this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
for (const waitTask of this._waitTasks) { this.#taskManager.terminateAll(
waitTask.terminate( new Error('waitForFunction failed: frame got detached.')
new Error('waitForFunction failed: frame got detached.') );
);
}
} }
executionContext(): Promise<ExecutionContext> { executionContext(): Promise<ExecutionContext> {
@ -411,8 +409,6 @@ export class IsolatedWorld {
// TODO: In theory, it would be enough to call this just once // TODO: In theory, it would be enough to call this just once
await context._client.send('Runtime.addBinding', { await context._client.send('Runtime.addBinding', {
name, name,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The protocol definition is not up to date.
executionContextName: context._contextName, executionContextName: context._contextName,
}); });
await context.evaluate(expression); await context.evaluate(expression);
@ -420,18 +416,19 @@ export class IsolatedWorld {
// We could have tried to evaluate in a context which was already // We could have tried to evaluate in a context which was already
// destroyed. This happens, for example, if the page is navigated while // destroyed. This happens, for example, if the page is navigated while
// we are trying to add the binding // we are trying to add the binding
const ctxDestroyed = (error as Error).message.includes( if (error instanceof Error) {
'Execution context was destroyed' // Destroyed context.
); if (error.message.includes('Execution context was destroyed')) {
const ctxNotFound = (error as Error).message.includes( return;
'Cannot find context with specified id' }
); // Missing context.
if (ctxDestroyed || ctxNotFound) { if (error.message.includes('Cannot find context with specified id')) {
return; return;
} else { }
debugError(error);
return;
} }
debugError(error);
return;
} }
this.#ctxBindings.add( this.#ctxBindings.add(
IsolatedWorld.#bindingIdentifier(name, context._contextId) IsolatedWorld.#bindingIdentifier(name, context._contextId)
@ -476,7 +473,17 @@ export class IsolatedWorld {
throw new Error(`Bound function $name is not found`); throw new Error(`Bound function $name is not found`);
} }
const result = await fn(...args); const result = await fn(...args);
await context.evaluate(deliverResult, name, seq, result); await context.evaluate(
(name: string, seq: number, result: unknown) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = self[name].callbacks;
callbacks.get(seq).resolve(result);
callbacks.delete(seq);
},
name,
seq,
result
);
} catch (error) { } catch (error) {
// The WaitTask may already have been resolved by timing out, or the // The WaitTask may already have been resolved by timing out, or the
// exection context may have been destroyed. // exection context may have been destroyed.
@ -488,14 +495,6 @@ export class IsolatedWorld {
} }
debugError(error); debugError(error);
} }
function deliverResult(name: string, seq: number, result: unknown): void {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Code is evaluated in a different context.
(globalThis as any)[name].callbacks.get(seq).resolve(result);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Code is evaluated in a different context.
(globalThis as any)[name].callbacks.delete(seq);
}
}; };
async _waitForSelectorInPage( async _waitForSelectorInPage(
@ -503,59 +502,97 @@ export class IsolatedWorld {
root: ElementHandle<Node> | undefined, root: ElementHandle<Node> | undefined,
selector: string, selector: string,
options: WaitForSelectorOptions, options: WaitForSelectorOptions,
binding?: PageBinding bindings = new Set<(...args: never[]) => unknown>()
): Promise<JSHandle<unknown> | null> { ): Promise<JSHandle<unknown> | null> {
const { const {
visible: waitForVisible = false, visible: waitForVisible = false,
hidden: waitForHidden = false, hidden: waitForHidden = false,
timeout = this.#timeoutSettings.timeout(), timeout = this.#timeoutSettings.timeout(),
} = options; } = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `selector \`${selector}\`${ try {
waitForHidden ? ' to be hidden' : '' const handle = await this.waitForFunction(
}`; async (PuppeteerUtil, query, selector, root, visible) => {
async function predicate( if (!PuppeteerUtil) {
root: Element | Document, return;
selector: string, }
waitForVisible: boolean, const node = (await PuppeteerUtil.createFunction(query)(
waitForHidden: boolean root || document,
): Promise<Node | null | boolean> { selector,
const node = (await predicateQueryHandler(root, selector)) as Element; PuppeteerUtil
return checkWaitForOptions(node, waitForVisible, waitForHidden); )) as Node | null;
return PuppeteerUtil.checkVisibility(node, visible);
},
{
bindings,
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
root,
timeout,
},
new LazyArg(async () => {
try {
// In case CDP fails.
return await this.puppeteerUtil;
} catch {
return undefined;
}
}),
queryOne.toString(),
selector,
root,
waitForVisible ? true : waitForHidden ? false : undefined
);
const elementHandle = handle.asElement();
if (!elementHandle) {
await handle.dispose();
return null;
}
return elementHandle;
} catch (error) {
if (!isErrorLike(error)) {
throw error;
}
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
throw error;
} }
const waitTaskOptions: WaitTaskOptions = {
isolatedWorld: this,
predicateBody: makePredicateString(predicate, queryOne),
predicateAcceptsContextElement: true,
title,
polling,
timeout,
args: [selector, waitForVisible, waitForHidden],
binding,
root,
};
const waitTask = new WaitTask(waitTaskOptions);
return waitTask.promise;
} }
waitForFunction( waitForFunction<
pageFunction: Function | string, Params extends unknown[],
options: {polling?: string | number; timeout?: number} = {}, Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
...args: unknown[] >(
): Promise<JSHandle> { pageFunction: Func | string,
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} = options: {
options; polling?: 'raf' | 'mutation' | number;
const waitTaskOptions: WaitTaskOptions = { timeout?: number;
isolatedWorld: this, root?: ElementHandle<Node>;
predicateBody: pageFunction, bindings?: Set<(...args: never[]) => unknown>;
predicateAcceptsContextElement: false, } = {},
title: 'function', ...args: Params
polling, ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
timeout, const {
args, polling = 'raf',
}; timeout = this.#timeoutSettings.timeout(),
const waitTask = new WaitTask(waitTaskOptions); bindings,
return waitTask.promise; root,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
bindings,
polling,
root,
timeout,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return waitTask.result;
} }
async title(): Promise<string> { async title(): Promise<string> {
@ -593,315 +630,3 @@ export class IsolatedWorld {
return result; return result;
} }
} }
/**
* @internal
*/
export interface WaitTaskOptions {
isolatedWorld: IsolatedWorld;
predicateBody: Function | string;
predicateAcceptsContextElement: boolean;
title: string;
polling: string | number;
timeout: number;
binding?: PageBinding;
args: unknown[];
root?: ElementHandle<Node>;
}
const noop = (): void => {};
/**
* @internal
*/
export class WaitTask {
#isolatedWorld: IsolatedWorld;
#polling: 'raf' | 'mutation' | number;
#timeout: number;
#predicateBody: string;
#predicateAcceptsContextElement: boolean;
#args: unknown[];
#binding?: PageBinding;
#runCount = 0;
#resolve: (x: JSHandle) => void = noop;
#reject: (x: Error) => void = noop;
#timeoutTimer?: NodeJS.Timeout;
#terminated = false;
#root: ElementHandle<Node> | null = null;
promise: Promise<JSHandle>;
constructor(options: WaitTaskOptions) {
if (isString(options.polling)) {
assert(
options.polling === 'raf' || options.polling === 'mutation',
'Unknown polling option: ' + options.polling
);
} else if (isNumber(options.polling)) {
assert(
options.polling > 0,
'Cannot poll with non-positive interval: ' + options.polling
);
} else {
throw new Error('Unknown polling options: ' + options.polling);
}
function getPredicateBody(predicateBody: Function | string) {
if (isString(predicateBody)) {
return `return (${predicateBody});`;
}
return `return (${predicateBody})(...args);`;
}
this.#isolatedWorld = options.isolatedWorld;
this.#polling = options.polling;
this.#timeout = options.timeout;
this.#root = options.root || null;
this.#predicateBody = getPredicateBody(options.predicateBody);
this.#predicateAcceptsContextElement =
options.predicateAcceptsContextElement;
this.#args = options.args;
this.#binding = options.binding;
this.#runCount = 0;
this.#isolatedWorld._waitTasks.add(this);
if (this.#binding) {
this.#isolatedWorld._boundFunctions.set(
this.#binding.name,
this.#binding.pptrFunction
);
}
this.promise = new Promise<JSHandle>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
});
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
if (options.timeout) {
const timeoutError = new TimeoutError(
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
);
this.#timeoutTimer = setTimeout(() => {
return this.terminate(timeoutError);
}, options.timeout);
}
this.rerun();
}
terminate(error: Error): void {
this.#terminated = true;
this.#reject(error);
this.#cleanup();
}
async rerun(): Promise<void> {
const runCount = ++this.#runCount;
let success: JSHandle | null = null;
let error: Error | null = null;
const context = await this.#isolatedWorld.executionContext();
if (this.#terminated || runCount !== this.#runCount) {
return;
}
if (this.#binding) {
await this.#isolatedWorld._addBindingToContext(
context,
this.#binding.name
);
}
if (this.#terminated || runCount !== this.#runCount) {
return;
}
try {
success = await context.evaluateHandle(
waitForPredicatePageFunction,
this.#root || null,
this.#predicateBody,
this.#predicateAcceptsContextElement,
this.#polling,
this.#timeout,
...this.#args
);
} catch (error_) {
error = error_ as Error;
}
if (this.#terminated || runCount !== this.#runCount) {
if (success) {
await success.dispose();
}
return;
}
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if (
!error &&
(await this.#isolatedWorld
.evaluate(s => {
return !s;
}, success)
.catch(() => {
return true;
}))
) {
if (!success) {
throw new Error('Assertion: result handle is not available');
}
await success.dispose();
return;
}
if (error) {
if (error.message.includes('TypeError: binding is not a function')) {
return this.rerun();
}
// When frame is detached the task should have been terminated by the IsolatedWorld.
// This can fail if we were adding this task while the frame was detached,
// so we terminate here instead.
if (
error.message.includes(
'Execution context is not available in detached frame'
)
) {
this.terminate(
new Error('waitForFunction failed: frame got detached.')
);
return;
}
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if (error.message.includes('Execution context was destroyed')) {
return;
}
// We could have tried to evaluate in a context which was already
// destroyed.
if (error.message.includes('Cannot find context with specified id')) {
return;
}
this.#reject(error);
} else {
if (!success) {
throw new Error('Assertion: result handle is not available');
}
this.#resolve(success);
}
this.#cleanup();
}
#cleanup(): void {
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
this.#isolatedWorld._waitTasks.delete(this);
}
}
async function waitForPredicatePageFunction(
root: Node | null,
predicateBody: string,
predicateAcceptsContextElement: boolean,
polling: 'raf' | 'mutation' | number,
timeout: number,
...args: unknown[]
): Promise<unknown> {
root = root || document;
const predicate = new Function('...args', predicateBody);
let timedOut = false;
if (timeout) {
setTimeout(() => {
return (timedOut = true);
}, timeout);
}
switch (polling) {
case 'raf':
return await pollRaf();
case 'mutation':
return await pollMutation();
default:
return await pollInterval(polling);
}
async function pollMutation(): Promise<unknown> {
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
return Promise.resolve(success);
}
let fulfill = (_?: unknown) => {};
const result = new Promise(x => {
return (fulfill = x);
});
const observer = new MutationObserver(async () => {
if (timedOut) {
observer.disconnect();
fulfill();
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
if (!root) {
throw new Error('Root element is not found.');
}
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
});
return result;
}
async function pollRaf(): Promise<unknown> {
let fulfill = (_?: unknown): void => {};
const result = new Promise(x => {
return (fulfill = x);
});
await onRaf();
return result;
async function onRaf(): Promise<void> {
if (timedOut) {
fulfill();
return;
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
fulfill(success);
} else {
requestAnimationFrame(onRaf);
}
}
}
async function pollInterval(pollInterval: number): Promise<unknown> {
let fulfill = (_?: unknown): void => {};
const result = new Promise(x => {
return (fulfill = x);
});
await onTimeout();
return result;
async function onTimeout(): Promise<void> {
if (timedOut) {
fulfill();
return;
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
fulfill(success);
} else {
setTimeout(onTimeout, pollInterval);
}
}
}
}

View file

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

View file

@ -1,3 +1,19 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';

View file

@ -15,7 +15,6 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
@ -25,6 +24,7 @@ import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
import {debugError, isString} from './util.js'; import {debugError, isString} from './util.js';
import {DeferredPromise} from '../util/DeferredPromise.js'; import {DeferredPromise} from '../util/DeferredPromise.js';
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js'; import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
import {CDPSession} from './Connection.js';
/** /**
* @public * @public
@ -66,13 +66,6 @@ export const NetworkManagerEmittedEvents = {
RequestFinished: Symbol('NetworkManager.RequestFinished'), RequestFinished: Symbol('NetworkManager.RequestFinished'),
} as const; } as const;
interface CDPSession extends EventEmitter {
send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']>;
}
interface FrameManager { interface FrameManager {
frame(frameId: string): Frame | null; frame(frameId: string): Frame | null;
} }

View file

@ -23,8 +23,12 @@ import {
} from '../util/DeferredPromise.js'; } from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {Accessibility} from './Accessibility.js'; import {Accessibility} from './Accessibility.js';
import {Browser, BrowserContext} from './Browser.js'; import type {Browser, BrowserContext} from '../api/Browser.js';
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js'; import {
CDPSession,
CDPSessionEmittedEvents,
isTargetClosedError,
} from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js'; import {Coverage} from './Coverage.js';
import {Dialog} from './Dialog.js'; import {Dialog} from './Dialog.js';
@ -36,6 +40,7 @@ import {
Frame, Frame,
FrameAddScriptTagOptions, FrameAddScriptTagOptions,
FrameAddStyleTagOptions, FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js'; } from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
@ -470,7 +475,15 @@ export class Page extends EventEmitter {
); );
await page.#initialize(); await page.#initialize();
if (defaultViewport) { if (defaultViewport) {
await page.setViewport(defaultViewport); try {
await page.setViewport(defaultViewport);
} catch (err) {
if (isErrorLike(err) && isTargetClosedError(err)) {
debugError(err);
} else {
throw err;
}
}
} }
return page; return page;
} }
@ -645,11 +658,19 @@ export class Page extends EventEmitter {
}; };
async #initialize(): Promise<void> { async #initialize(): Promise<void> {
await Promise.all([ try {
this.#frameManager.initialize(this.#target._targetId), await Promise.all([
this.#client.send('Performance.enable'), this.#frameManager.initialize(),
this.#client.send('Log.enable'), this.#client.send('Performance.enable'),
]); this.#client.send('Log.enable'),
]);
} catch (err) {
if (isErrorLike(err) && isTargetClosedError(err)) {
debugError(err);
} else {
throw err;
}
}
} }
async #onFileChooser( async #onFileChooser(
@ -3544,32 +3565,14 @@ export class Page extends EventEmitter {
* ``` * ```
* *
* @param pageFunction - Function to be evaluated in browser context * @param pageFunction - Function to be evaluated in browser context
* @param options - Optional waiting parameters * @param options - Options for configuring waiting behavior.
*
* - `polling` - An interval at which the `pageFunction` is executed, defaults
* to `raf`. If `polling` is a number, then it is treated as an interval in
* milliseconds at which the function would be executed. If polling is a
* string, then it can be one of the following values:
* - `raf` - to constantly execute `pageFunction` in
* `requestAnimationFrame` callback. This is the tightest polling mode
* which is suitable to observe styling changes.
* - `mutation`- to execute pageFunction on every DOM mutation.
* - `timeout` - maximum time to wait for in milliseconds. Defaults to `30000`
* (30 seconds). Pass `0` to disable timeout. The default value can be
* changed by using the {@link Page.setDefaultTimeout} method.
* @param args - Arguments to pass to `pageFunction`
* @returns A `Promise` which resolves to a JSHandle/ElementHandle of the the
* `pageFunction`'s return value.
*/ */
waitForFunction< waitForFunction<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>( >(
pageFunction: Func | string, pageFunction: Func | string,
options: { options: FrameWaitForFunctionOptions = {},
timeout?: number;
polling?: string | number;
} = {},
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args); return this.mainFrame().waitForFunction(pageFunction, options, ...args);

View file

@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import {Browser} from './Browser.js'; import {Browser} from '../api/Browser.js';
import {BrowserConnectOptions, _connectToBrowser} from './BrowserConnector.js'; import {
BrowserConnectOptions,
_connectToCDPBrowser,
} from './BrowserConnector.js';
import {ConnectionTransport} from './ConnectionTransport.js'; import {ConnectionTransport} from './ConnectionTransport.js';
import {devices} from './DeviceDescriptors.js'; import {devices} from './DeviceDescriptors.js';
import {errors} from './Errors.js'; import {errors} from './Errors.js';
@ -54,7 +57,13 @@ export interface ConnectOptions extends BrowserConnectOptions {
* @public * @public
*/ */
export class Puppeteer { export class Puppeteer {
/**
* @internal
*/
protected _isPuppeteerCore: boolean; protected _isPuppeteerCore: boolean;
/**
* @internal
*/
protected _changedProduct = false; protected _changedProduct = false;
/** /**
@ -75,7 +84,7 @@ export class Puppeteer {
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
connect(options: ConnectOptions): Promise<Browser> { connect(options: ConnectOptions): Promise<Browser> {
return _connectToBrowser(options); return _connectToCDPBrowser(options);
} }
/** /**

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import PuppeteerUtil from '../injected/injected.js';
import {ariaHandler} from './AriaQueryHandler.js'; import {ariaHandler} from './AriaQueryHandler.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
@ -41,6 +42,28 @@ export interface CustomQueryHandler {
* @internal * @internal
*/ */
export interface InternalQueryHandler { export interface InternalQueryHandler {
/**
* @returns A {@link Node} matching the given `selector` from {@link node}.
*/
queryOne?: (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil
) => Node | null;
/**
* @returns Some {@link Node}s matching the given `selector` from {@link node}.
*/
queryAll?: (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil
) => Node[];
}
/**
* @internal
*/
export interface PuppeteerQueryHandler {
/** /**
* Queries for a single node given a selector and {@link ElementHandle}. * Queries for a single node given a selector and {@link ElementHandle}.
* *
@ -71,15 +94,19 @@ export interface InternalQueryHandler {
) => Promise<ElementHandle<Node> | null>; ) => Promise<ElementHandle<Node> | null>;
} }
function internalizeCustomQueryHandler( function createPuppeteerQueryHandler(
handler: CustomQueryHandler handler: InternalQueryHandler
): InternalQueryHandler { ): PuppeteerQueryHandler {
const internalHandler: InternalQueryHandler = {}; const internalHandler: PuppeteerQueryHandler = {};
if (handler.queryOne) { if (handler.queryOne) {
const queryOne = handler.queryOne; const queryOne = handler.queryOne;
internalHandler.queryOne = async (element, selector) => { internalHandler.queryOne = async (element, selector) => {
const jsHandle = await element.evaluateHandle(queryOne, selector); const jsHandle = await element.evaluateHandle(
queryOne,
selector,
await element.executionContext()._world!.puppeteerUtil
);
const elementHandle = jsHandle.asElement(); const elementHandle = jsHandle.asElement();
if (elementHandle) { if (elementHandle) {
return elementHandle; return elementHandle;
@ -121,7 +148,11 @@ function internalizeCustomQueryHandler(
if (handler.queryAll) { if (handler.queryAll) {
const queryAll = handler.queryAll; const queryAll = handler.queryAll;
internalHandler.queryAll = async (element, selector) => { internalHandler.queryAll = async (element, selector) => {
const jsHandle = await element.evaluateHandle(queryAll, selector); const jsHandle = await element.evaluateHandle(
queryAll,
selector,
await element.executionContext()._world!.puppeteerUtil
);
const properties = await jsHandle.getProperties(); const properties = await jsHandle.getProperties();
await jsHandle.dispose(); await jsHandle.dispose();
const result = []; const result = [];
@ -138,7 +169,7 @@ function internalizeCustomQueryHandler(
return internalHandler; return internalHandler;
} }
const defaultHandler = internalizeCustomQueryHandler({ const defaultHandler = createPuppeteerQueryHandler({
queryOne: (element, selector) => { queryOne: (element, selector) => {
if (!('querySelector' in element)) { if (!('querySelector' in element)) {
throw new Error( throw new Error(
@ -165,87 +196,35 @@ const defaultHandler = internalizeCustomQueryHandler({
}, },
}); });
const pierceHandler = internalizeCustomQueryHandler({ const pierceHandler = createPuppeteerQueryHandler({
queryOne: (element, selector) => { queryOne: (element, selector, {pierceQuerySelector}) => {
let found: Node | null = null; return pierceQuerySelector(element, selector);
const search = (root: Node) => {
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
const currentNode = iter.currentNode as HTMLElement;
if (currentNode.shadowRoot) {
search(currentNode.shadowRoot);
}
if (currentNode instanceof ShadowRoot) {
continue;
}
if (currentNode !== root && !found && currentNode.matches(selector)) {
found = currentNode;
}
} while (!found && iter.nextNode());
};
if (element instanceof Document) {
element = element.documentElement;
}
search(element);
return found;
}, },
queryAll: (element, selector, {pierceQuerySelectorAll}) => {
queryAll: (element, selector) => { return pierceQuerySelectorAll(element, selector);
const result: Node[] = [];
const collect = (root: Node) => {
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
const currentNode = iter.currentNode as HTMLElement;
if (currentNode.shadowRoot) {
collect(currentNode.shadowRoot);
}
if (currentNode instanceof ShadowRoot) {
continue;
}
if (currentNode !== root && currentNode.matches(selector)) {
result.push(currentNode);
}
} while (iter.nextNode());
};
if (element instanceof Document) {
element = element.documentElement;
}
collect(element);
return result;
}, },
}); });
const xpathHandler = internalizeCustomQueryHandler({ const xpathHandler = createPuppeteerQueryHandler({
queryOne: (element, selector) => { queryOne: (element, selector, {xpathQuerySelector}) => {
const doc = element.ownerDocument || document; return xpathQuerySelector(element, selector);
const result = doc.evaluate(
selector,
element,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE
);
return result.singleNodeValue;
}, },
queryAll: (element, selector, {xpathQuerySelectorAll}) => {
return xpathQuerySelectorAll(element, selector);
},
});
queryAll: (element, selector) => { const textQueryHandler = createPuppeteerQueryHandler({
const doc = element.ownerDocument || document; queryOne: (element, selector, {textQuerySelector}) => {
const iterator = doc.evaluate( return textQuerySelector(element, selector);
selector, },
element, queryAll: (element, selector, {textQuerySelectorAll}) => {
null, return textQuerySelectorAll(element, selector);
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array: Node[] = [];
let item;
while ((item = iterator.iterateNext())) {
array.push(item);
}
return array;
}, },
}); });
interface RegisteredQueryHandler { interface RegisteredQueryHandler {
handler: InternalQueryHandler; handler: PuppeteerQueryHandler;
transformSelector?: (selector: string) => string; transformSelector?: (selector: string) => string;
} }
@ -253,6 +232,7 @@ const INTERNAL_QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>([
['aria', {handler: ariaHandler}], ['aria', {handler: ariaHandler}],
['pierce', {handler: pierceHandler}], ['pierce', {handler: pierceHandler}],
['xpath', {handler: xpathHandler}], ['xpath', {handler: xpathHandler}],
['text', {handler: textQueryHandler}],
]); ]);
const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>(); const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>();
@ -294,7 +274,7 @@ export function registerCustomQueryHandler(
throw new Error(`Custom query handler names may only contain [a-zA-Z]`); throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
} }
QUERY_HANDLERS.set(name, {handler: internalizeCustomQueryHandler(handler)}); QUERY_HANDLERS.set(name, {handler: createPuppeteerQueryHandler(handler)});
} }
/** /**
@ -331,7 +311,7 @@ const CUSTOM_QUERY_SEPARATORS = ['=', '/'];
*/ */
export function getQueryHandlerAndSelector(selector: string): { export function getQueryHandlerAndSelector(selector: string): {
updatedSelector: string; updatedSelector: string;
queryHandler: InternalQueryHandler; queryHandler: PuppeteerQueryHandler;
} { } {
for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) { for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) {
for (const [ for (const [

View file

@ -17,7 +17,11 @@
import {Page, PageEmittedEvents} from './Page.js'; import {Page, PageEmittedEvents} from './Page.js';
import {WebWorker} from './WebWorker.js'; import {WebWorker} from './WebWorker.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {Browser, BrowserContext, IsPageTargetCallback} from './Browser.js'; import type {
Browser,
BrowserContext,
IsPageTargetCallback,
} from '../api/Browser.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {TaskQueue} from './TaskQueue.js'; import {TaskQueue} from './TaskQueue.js';

View file

@ -0,0 +1,257 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Poller} from '../injected/Poller.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {ElementHandle} from './ElementHandle.js';
import {TimeoutError} from './Errors.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {HandleFor} from './types.js';
/**
* @internal
*/
export interface WaitTaskOptions {
bindings?: Set<(...args: never[]) => unknown>;
polling: 'raf' | 'mutation' | number;
root?: ElementHandle<Node>;
timeout: number;
}
/**
* @internal
*/
export class WaitTask<T = unknown> {
#world: IsolatedWorld;
#bindings: Set<(...args: never[]) => unknown>;
#polling: 'raf' | 'mutation' | number;
#root?: ElementHandle<Node>;
#fn: string;
#args: unknown[];
#timeout?: NodeJS.Timeout;
#result = createDeferredPromise<HandleFor<T>>();
#poller?: JSHandle<Poller<T>>;
constructor(
world: IsolatedWorld,
options: WaitTaskOptions,
fn: ((...args: unknown[]) => Promise<T>) | string,
...args: unknown[]
) {
this.#world = world;
this.#bindings = options.bindings ?? new Set();
this.#polling = options.polling;
this.#root = options.root;
switch (typeof fn) {
case 'string':
this.#fn = `() => {return (${fn});}`;
break;
default:
this.#fn = fn.toString();
break;
}
this.#args = args;
this.#world.taskManager.add(this);
if (options.timeout) {
this.#timeout = setTimeout(() => {
this.terminate(
new TimeoutError(`Waiting failed: ${options.timeout}ms exceeded`)
);
}, options.timeout);
}
if (this.#bindings.size !== 0) {
for (const fn of this.#bindings) {
this.#world._boundFunctions.set(fn.name, fn);
}
}
this.rerun();
}
get result(): Promise<HandleFor<T>> {
return this.#result;
}
async rerun(): Promise<void> {
try {
if (this.#bindings.size !== 0) {
const context = await this.#world.executionContext();
await Promise.all(
[...this.#bindings].map(async ({name}) => {
return await this.#world._addBindingToContext(context, name);
})
);
}
switch (this.#polling) {
case 'raf':
this.#poller = await this.#world.evaluateHandle(
({RAFPoller, createFunction}, fn, ...args) => {
const fun = createFunction(fn);
return new RAFPoller(() => {
return fun(...args) as Promise<T>;
});
},
await this.#world.puppeteerUtil,
this.#fn,
...this.#args
);
break;
case 'mutation':
this.#poller = await this.#world.evaluateHandle(
({MutationPoller, createFunction}, root, fn, ...args) => {
const fun = createFunction(fn);
return new MutationPoller(() => {
return fun(...args) as Promise<T>;
}, root || document);
},
await this.#world.puppeteerUtil,
this.#root,
this.#fn,
...this.#args
);
break;
default:
this.#poller = await this.#world.evaluateHandle(
({IntervalPoller, createFunction}, ms, fn, ...args) => {
const fun = createFunction(fn);
return new IntervalPoller(() => {
return fun(...args) as Promise<T>;
}, ms);
},
await this.#world.puppeteerUtil,
this.#polling,
this.#fn,
...this.#args
);
break;
}
await this.#poller.evaluate(poller => {
poller.start();
});
const result = await this.#poller.evaluateHandle(poller => {
return poller.result();
});
this.#result.resolve(result);
await this.terminate();
} catch (error) {
const badError = this.getBadError(error);
if (badError) {
await this.terminate(badError);
}
}
}
async terminate(error?: unknown): Promise<void> {
this.#world.taskManager.delete(this);
if (this.#timeout) {
clearTimeout(this.#timeout);
}
if (error && !this.#result.finished()) {
this.#result.reject(error);
}
if (this.#poller) {
try {
await this.#poller.evaluateHandle(async poller => {
await poller.stop();
});
if (this.#poller) {
await this.#poller.dispose();
this.#poller = undefined;
}
} catch {
// Ignore errors since they most likely come from low-level cleanup.
}
}
}
/**
* Not all errors lead to termination. They usually imply we need to rerun the task.
*/
getBadError(error: unknown): unknown {
if (error instanceof Error) {
// When frame is detached the task should have been terminated by the IsolatedWorld.
// This can fail if we were adding this task while the frame was detached,
// so we terminate here instead.
if (
error.message.includes(
'Execution context is not available in detached frame'
)
) {
return new Error('Waiting failed: Frame detached');
}
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if (error.message.includes('Execution context was destroyed')) {
return;
}
// We could have tried to evaluate in a context which was already
// destroyed.
if (error.message.includes('Cannot find context with specified id')) {
return;
}
}
return error;
}
}
/**
* @internal
*/
export class TaskManager {
#tasks: Set<WaitTask> = new Set<WaitTask>();
add(task: WaitTask<any>): void {
this.#tasks.add(task);
}
delete(task: WaitTask<any>): void {
this.#tasks.delete(task);
}
terminateAll(error?: Error): void {
for (const task of this.#tasks) {
task.terminate(error);
}
this.#tasks.clear();
}
async rerunAll(): Promise<void> {
await Promise.all(
[...this.#tasks].map(task => {
return task.rerun();
})
);
}
}

View file

@ -0,0 +1,52 @@
import {
Browser as BrowserBase,
BrowserCloseCallback,
} from '../../api/Browser.js';
import {Connection} from './Connection.js';
import {ChildProcess} from 'child_process';
/**
* @internal
*/
export class Browser extends BrowserBase {
/**
* @internal
*/
static async create(opts: Options): Promise<Browser> {
// TODO: await until the connection is established.
return new Browser(opts);
}
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#connection: Connection;
/**
* @internal
*/
constructor(opts: Options) {
super();
this.#process = opts.process;
this.#closeCallback = opts.closeCallback;
this.#connection = opts.connection;
}
override async close(): Promise<void> {
await this.#closeCallback?.call(null);
this.#connection.dispose();
}
override isConnected(): boolean {
return !this.#connection.closed;
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
}
interface Options {
process?: ChildProcess;
closeCallback?: BrowserCloseCallback;
connection: Connection;
}

View file

@ -0,0 +1,167 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {debug} from '../Debug.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
import {ConnectionTransport} from '../ConnectionTransport.js';
import {EventEmitter} from '../EventEmitter.js';
import {ProtocolError} from '../Errors.js';
import {ConnectionCallback} from '../Connection.js';
interface Command {
id: number;
method: string;
params: object;
}
interface CommandResponse {
id: number;
result: object;
}
interface ErrorResponse {
id: number;
error: string;
message: string;
stacktrace?: string;
}
interface Event {
method: string;
params: object;
}
/**
* @internal
*/
export class Connection extends EventEmitter {
#transport: ConnectionTransport;
#delay: number;
#lastId = 0;
#closed = false;
#callbacks: Map<number, ConnectionCallback> = new Map();
constructor(transport: ConnectionTransport, delay = 0) {
super();
this.#delay = delay;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.#onClose.bind(this);
}
get closed(): boolean {
return this.#closed;
}
send(method: string, params: object): Promise<any> {
const id = ++this.#lastId;
const stringifiedMessage = JSON.stringify({
id,
method,
params,
} as Command);
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
return new Promise((resolve, reject) => {
this.#callbacks.set(id, {
resolve,
reject,
error: new ProtocolError(),
method,
});
});
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object = JSON.parse(message) as
| Event
| ErrorResponse
| CommandResponse;
if ('id' in object) {
const callback = this.#callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this.#callbacks.delete(object.id);
if ('error' in object) {
callback.reject(
createProtocolError(callback.error, callback.method, object)
);
} else {
callback.resolve(object.result);
}
}
} else {
this.emit(object.method, object.params);
}
}
#onClose(): void {
if (this.#closed) {
return;
}
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
for (const callback of this.#callbacks.values()) {
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Connection closed.`
)
);
}
this.#callbacks.clear();
}
dispose(): void {
this.#onClose();
this.#transport.close();
}
}
function rewriteError(
error: ProtocolError,
message: string,
originalMessage?: string
): Error {
error.message = message;
error.originalMessage = originalMessage ?? error.originalMessage;
return error;
}
function createProtocolError(
error: ProtocolError,
method: string,
object: ErrorResponse
): Error {
let message = `Protocol error (${method}): ${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return rewriteError(error, message, object.message);
}

View file

@ -16,6 +16,7 @@
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {LazyArg} from './LazyArg.js';
/** /**
* @public * @public
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
* @public * @public
*/ */
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never; export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
/**
* @internal
*/
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
/** /**
* @public * @public
*/ */
export type InnerParams<T extends unknown[]> = { export type InnerParams<T extends unknown[]> = {
[K in keyof T]: FlattenHandle<T[K]>; [K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
}; };
/** /**

View file

@ -249,28 +249,26 @@ export function evaluationString(
* @internal * @internal
*/ */
export function pageBindingInitString(type: string, name: string): string { export function pageBindingInitString(type: string, name: string): string {
function addPageBinding(type: string, bindingName: string): void { function addPageBinding(type: string, name: string): void {
/* Cast window to any here as we're about to add properties to it // This is the CDP binding.
* via win[bindingName] which TypeScript doesn't like. // @ts-expect-error: In a different context.
*/ const callCDP = self[name];
const win = window as any;
const binding = win[bindingName];
win[bindingName] = (...args: unknown[]): Promise<unknown> => { // We replace the CDP binding with a Puppeteer binding.
const me = (window as any)[bindingName]; Object.assign(self, {
let callbacks = me.callbacks; [name](...args: unknown[]): Promise<unknown> {
if (!callbacks) { // This is the Puppeteer binding.
callbacks = new Map(); // @ts-expect-error: In a different context.
me.callbacks = callbacks; const callPuppeteer = self[name];
} callPuppeteer.callbacks ??= new Map();
const seq = (me.lastSeq || 0) + 1; const seq = (callPuppeteer.lastSeq ?? 0) + 1;
me.lastSeq = seq; callPuppeteer.lastSeq = seq;
const promise = new Promise((resolve, reject) => { callCDP(JSON.stringify({type, name, seq, args}));
return callbacks.set(seq, {resolve, reject}); return new Promise((resolve, reject) => {
}); callPuppeteer.callbacks.set(seq, {resolve, reject});
binding(JSON.stringify({type, name: bindingName, seq, args})); });
return promise; },
}; });
} }
return evaluationString(addPageBinding, type, name); return evaluationString(addPageBinding, type, name);
} }
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
return evaluationString(deliverErrorValue, name, seq, value); return evaluationString(deliverErrorValue, name, seq, value);
} }
/**
* @internal
*/
export function makePredicateString(
predicate: Function,
predicateQueryHandler: Function
): string {
function checkWaitForOptions(
node: Node | null,
waitForVisible: boolean,
waitForHidden: boolean
): Node | null | boolean {
if (!node) {
return waitForHidden;
}
if (!waitForVisible && !waitForHidden) {
return node;
}
const element =
node.nodeType === Node.TEXT_NODE
? (node.parentElement as Element)
: (node as Element);
const style = window.getComputedStyle(element);
const isVisible =
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success =
waitForVisible === isVisible || waitForHidden === !isVisible;
return success ? node : null;
function hasVisibleBoundingBox(): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
return `
(() => {
const predicateQueryHandler = ${predicateQueryHandler};
const checkWaitForOptions = ${checkWaitForOptions};
return (${predicate})(...args)
})() `;
}
/** /**
* @internal * @internal
*/ */

View file

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

View file

@ -1,3 +1,19 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {dirname} from 'path'; import {dirname} from 'path';
import {puppeteerDirname} from './compat.js'; import {puppeteerDirname} from './compat.js';

View file

@ -1,4 +1,4 @@
/** /**
* @internal * @internal
*/ */
export const packageVersion = '17.1.2'; export const packageVersion = '18.0.0';

View file

@ -0,0 +1,67 @@
// Copyright 2022 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export const pierceQuerySelector = (
root: Node,
selector: string
): Element | null => {
let found: Node | null = null;
const search = (root: Node) => {
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
const currentNode = iter.currentNode as Element;
if (currentNode.shadowRoot) {
search(currentNode.shadowRoot);
}
if (currentNode instanceof ShadowRoot) {
continue;
}
if (currentNode !== root && !found && currentNode.matches(selector)) {
found = currentNode;
}
} while (!found && iter.nextNode());
};
if (root instanceof Document) {
root = root.documentElement;
}
search(root);
return found;
};
export const pierceQuerySelectorAll = (
element: Node,
selector: string
): Element[] => {
const result: Element[] = [];
const collect = (root: Node) => {
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
const currentNode = iter.currentNode as Element;
if (currentNode.shadowRoot) {
collect(currentNode.shadowRoot);
}
if (currentNode instanceof ShadowRoot) {
continue;
}
if (currentNode !== root && currentNode.matches(selector)) {
result.push(currentNode);
}
} while (iter.nextNode());
};
if (element instanceof Document) {
element = element.documentElement;
}
collect(element);
return result;
};

View file

@ -1,15 +1,37 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {assert} from '../util/assert.js';
import { import {
createDeferredPromise, createDeferredPromise,
DeferredPromise, DeferredPromise,
} from '../util/DeferredPromise.js'; } from '../util/DeferredPromise.js';
import {assert} from '../util/assert.js';
interface Poller<T> { /**
start(): Promise<T>; * @internal
*/
export interface Poller<T> {
start(): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
result(): Promise<T>; result(): Promise<T>;
} }
/**
* @internal
*/
export class MutationPoller<T> implements Poller<T> { export class MutationPoller<T> implements Poller<T> {
#fn: () => Promise<T>; #fn: () => Promise<T>;
@ -22,12 +44,12 @@ export class MutationPoller<T> implements Poller<T> {
this.#root = root; this.#root = root;
} }
async start(): Promise<T> { async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>()); const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn(); const result = await this.#fn();
if (result) { if (result) {
promise.resolve(result); promise.resolve(result);
return result; return;
} }
this.#observer = new MutationObserver(async () => { this.#observer = new MutationObserver(async () => {
@ -43,8 +65,6 @@ export class MutationPoller<T> implements Poller<T> {
subtree: true, subtree: true,
attributes: true, attributes: true,
}); });
return this.#promise;
} }
async stop(): Promise<void> { async stop(): Promise<void> {
@ -54,6 +74,7 @@ export class MutationPoller<T> implements Poller<T> {
} }
if (this.#observer) { if (this.#observer) {
this.#observer.disconnect(); this.#observer.disconnect();
this.#observer = undefined;
} }
} }
@ -70,12 +91,12 @@ export class RAFPoller<T> implements Poller<T> {
this.#fn = fn; this.#fn = fn;
} }
async start(): Promise<T> { async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>()); const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn(); const result = await this.#fn();
if (result) { if (result) {
promise.resolve(result); promise.resolve(result);
return result; return;
} }
const poll = async () => { const poll = async () => {
@ -91,8 +112,6 @@ export class RAFPoller<T> implements Poller<T> {
await this.stop(); await this.stop();
}; };
window.requestAnimationFrame(poll); window.requestAnimationFrame(poll);
return this.#promise;
} }
async stop(): Promise<void> { async stop(): Promise<void> {
@ -119,12 +138,12 @@ export class IntervalPoller<T> implements Poller<T> {
this.#ms = ms; this.#ms = ms;
} }
async start(): Promise<T> { async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>()); const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn(); const result = await this.#fn();
if (result) { if (result) {
promise.resolve(result); promise.resolve(result);
return result; return;
} }
this.#interval = setInterval(async () => { this.#interval = setInterval(async () => {
@ -135,8 +154,6 @@ export class IntervalPoller<T> implements Poller<T> {
promise.resolve(result); promise.resolve(result);
await this.stop(); await this.stop();
}, this.#ms); }, this.#ms);
return this.#promise;
} }
async stop(): Promise<void> { async stop(): Promise<void> {
@ -146,6 +163,7 @@ export class IntervalPoller<T> implements Poller<T> {
} }
if (this.#interval) { if (this.#interval) {
clearInterval(this.#interval); clearInterval(this.#interval);
this.#interval = undefined;
} }
} }

View file

@ -0,0 +1,153 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
interface NonTrivialValueNode extends Node {
value: string;
}
const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
/**
* Determines if the node has a non-trivial value property.
*/
const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => {
if (node instanceof HTMLSelectElement) {
return true;
}
if (node instanceof HTMLTextAreaElement) {
return true;
}
if (
node instanceof HTMLInputElement &&
!TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
) {
return true;
}
return false;
};
const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
/**
* Determines whether a given node is suitable for text matching.
*
* @internal
*/
export const isSuitableNodeForTextMatching = (node: Node): boolean => {
return (
!UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
);
};
/**
* @internal
*/
export type TextContent = {
// Contains the full text of the node.
full: string;
// Contains the text immediately beneath the node.
immediate: string[];
};
/**
* Maps {@link Node}s to their computed {@link TextContent}.
*/
const textContentCache = new WeakMap<Node, TextContent>();
const eraseFromCache = (node: Node | null) => {
while (node) {
textContentCache.delete(node);
if (node instanceof ShadowRoot) {
node = node.host;
} else {
node = node.parentNode;
}
}
};
/**
* Erases the cache when the tree has mutated text.
*/
const observedNodes = new WeakSet<Node>();
const textChangeObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
eraseFromCache(mutation.target);
}
});
/**
* Builds the text content of a node using some custom logic.
*
* @remarks
* The primary reason this function exists is due to {@link ShadowRoot}s not having
* text content.
*
* @internal
*/
export const createTextContent = (root: Node): TextContent => {
let value = textContentCache.get(root);
if (value) {
return value;
}
value = {full: '', immediate: []};
if (!isSuitableNodeForTextMatching(root)) {
return value;
}
let currentImmediate = '';
if (isNonTrivialValueNode(root)) {
value.full = root.value;
value.immediate.push(root.value);
root.addEventListener(
'input',
event => {
eraseFromCache(event.target as HTMLInputElement);
},
{once: true, capture: true}
);
} else {
for (let child = root.firstChild; child; child = child.nextSibling) {
if (child.nodeType === Node.TEXT_NODE) {
value.full += child.nodeValue ?? '';
currentImmediate += child.nodeValue ?? '';
continue;
}
if (currentImmediate) {
value.immediate.push(currentImmediate);
}
currentImmediate = '';
if (child.nodeType === Node.ELEMENT_NODE) {
value.full += createTextContent(child).full;
}
}
if (currentImmediate) {
value.immediate.push(currentImmediate);
}
if (root instanceof Element && root.shadowRoot) {
value.full += createTextContent(root.shadowRoot).full;
}
if (!observedNodes.has(root)) {
textChangeObserver.observe(root, {
childList: true,
characterData: true,
});
observedNodes.add(root);
}
}
textContentCache.set(root, value);
return value;
};

View file

@ -0,0 +1,86 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createTextContent,
isSuitableNodeForTextMatching,
} from './TextContent.js';
/**
* Queries the given node for a node matching the given text selector.
*
* @internal
*/
export const textQuerySelector = (
root: Node,
selector: string
): Element | null => {
for (const node of root.childNodes) {
if (node instanceof Element && isSuitableNodeForTextMatching(node)) {
let matchedNode: Element | null;
if (node.shadowRoot) {
matchedNode = textQuerySelector(node.shadowRoot, selector);
} else {
matchedNode = textQuerySelector(node, selector);
}
if (matchedNode) {
return matchedNode;
}
}
}
if (root instanceof Element) {
const textContent = createTextContent(root);
if (textContent.full.includes(selector)) {
return root;
}
}
return null;
};
/**
* Queries the given node for all nodes matching the given text selector.
*
* @internal
*/
export const textQuerySelectorAll = (
root: Node,
selector: string
): Element[] => {
let results: Element[] = [];
for (const node of root.childNodes) {
if (node instanceof Element) {
let matchedNodes: Element[];
if (node.shadowRoot) {
matchedNodes = textQuerySelectorAll(node.shadowRoot, selector);
} else {
matchedNodes = textQuerySelectorAll(node, selector);
}
results = results.concat(matchedNodes);
}
}
if (results.length > 0) {
return results;
}
if (root instanceof Element) {
const textContent = createTextContent(root);
if (textContent.full.includes(selector)) {
return [root];
}
}
return [];
};

View file

@ -0,0 +1,45 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const xpathQuerySelector = (
root: Node,
selector: string
): Node | null => {
const doc = root.ownerDocument || document;
const result = doc.evaluate(
selector,
root,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE
);
return result.singleNodeValue;
};
export const xpathQuerySelectorAll = (root: Node, selector: string): Node[] => {
const doc = root.ownerDocument || document;
const iterator = doc.evaluate(
selector,
root,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array: Node[] = [];
let item;
while ((item = iterator.iterateNext())) {
array.push(item);
}
return array;
};

View file

@ -1,14 +1,37 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {createDeferredPromise} from '../util/DeferredPromise.js'; import {createDeferredPromise} from '../util/DeferredPromise.js';
import * as Poller from './Poller.js'; import * as Poller from './Poller.js';
import * as TextContent from './TextContent.js';
import * as TextQuerySelector from './TextQuerySelector.js';
import * as XPathQuerySelector from './XPathQuerySelector.js';
import * as PierceQuerySelector from './PierceQuerySelector.js';
import * as util from './util.js'; import * as util from './util.js';
Object.assign( const PuppeteerUtil = Object.freeze({
self, ...util,
Object.freeze({ ...Poller,
InjectedUtil: { ...TextContent,
...Poller, ...TextQuerySelector,
...util, ...XPathQuerySelector,
createDeferredPromise, ...PierceQuerySelector,
}, createDeferredPromise,
}) });
);
type PuppeteerUtil = typeof PuppeteerUtil;
export default PuppeteerUtil;

View file

@ -1,7 +1,25 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const createdFunctions = new Map<string, (...args: unknown[]) => unknown>(); const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
/** /**
* Creates a function from a string. * Creates a function from a string.
*
* @internal
*/ */
export const createFunction = ( export const createFunction = (
functionValue: string functionValue: string
@ -16,3 +34,42 @@ export const createFunction = (
createdFunctions.set(functionValue, fn); createdFunctions.set(functionValue, fn);
return fn; return fn;
}; };
const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
/**
* @internal
*/
export const checkVisibility = (
node: Node | null,
visible?: boolean
): Node | boolean => {
if (!node) {
return visible === false;
}
if (visible === undefined) {
return node;
}
const element = (
node.nodeType === Node.TEXT_NODE ? node.parentElement : node
) as Element;
const style = window.getComputedStyle(element);
const isVisible =
style &&
!HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
isBoundingBoxVisible(element);
return visible === isVisible ? node : false;
};
function isBoundingBoxVisible(element: Element): boolean {
const rect = element.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.right > 0 &&
rect.bottom > 0 &&
rect.left < self.innerWidth &&
rect.top < self.innerHeight
);
}

View file

@ -237,7 +237,12 @@ export class BrowserFetcher {
this.#platform = 'linux'; this.#platform = 'linux';
break; break;
case 'win32': case 'win32':
this.#platform = os.arch() === 'x64' ? 'win64' : 'win32'; this.#platform =
os.arch() === 'x64' ||
// Windows 11 for ARM supports x64 emulation
(os.arch() === 'arm64' && _isWindows11(os.release()))
? 'win64'
: 'win32';
return; return;
default: default:
assert(false, 'Unsupported platform: ' + platform); assert(false, 'Unsupported platform: ' + platform);
@ -336,7 +341,7 @@ export class BrowserFetcher {
} }
// Use system Chromium builds on Linux ARM devices // Use system Chromium builds on Linux ARM devices
if (os.platform() !== 'darwin' && os.arch() === 'arm64') { if (os.platform() === 'linux' && os.arch() === 'arm64') {
handleArm64(); handleArm64();
return; return;
} }
@ -497,6 +502,25 @@ function parseFolderPath(
return {product, platform, revision}; return {product, platform, revision};
} }
/**
* Windows 11 is identified by 10.0.22000 or greater
* @internal
*/
function _isWindows11(version: string): boolean {
const parts = version.split('.');
if (parts.length > 2) {
const major = parseInt(parts[0] as string, 10);
const minor = parseInt(parts[1] as string, 10);
const patch = parseInt(parts[2] as string, 10);
return (
major > 10 ||
(major === 10 && minor > 0) ||
(major === 10 && minor === 0 && patch >= 22000)
);
}
return false;
}
/** /**
* @internal * @internal
*/ */

View file

@ -22,6 +22,7 @@ import removeFolder from 'rimraf';
import {promisify} from 'util'; import {promisify} from 'util';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Connection} from '../common/Connection.js'; import {Connection} from '../common/Connection.js';
import {Connection as BiDiConnection} from '../common/bidi/Connection.js';
import {debug} from '../common/Debug.js'; import {debug} from '../common/Debug.js';
import {TimeoutError} from '../common/Errors.js'; import {TimeoutError} from '../common/Errors.js';
import { import {
@ -245,6 +246,25 @@ export class BrowserRunner {
removeEventListeners(this.#listeners); removeEventListeners(this.#listeners);
} }
async setupWebDriverBiDiConnection(options: {
timeout: number;
slowMo: number;
preferredRevision: string;
}): Promise<BiDiConnection> {
assert(this.proc, 'BrowserRunner not started.');
const {timeout, slowMo, preferredRevision} = options;
let browserWSEndpoint = await waitForWSEndpoint(
this.proc,
timeout,
preferredRevision,
/^WebDriver BiDi listening on (ws:\/\/.*)$/
);
browserWSEndpoint += '/session';
const transport = await WebSocketTransport.create(browserWSEndpoint);
return new BiDiConnection(transport, slowMo);
}
async setupConnection(options: { async setupConnection(options: {
usePipe?: boolean; usePipe?: boolean;
timeout: number; timeout: number;
@ -279,7 +299,8 @@ export class BrowserRunner {
function waitForWSEndpoint( function waitForWSEndpoint(
browserProcess: childProcess.ChildProcess, browserProcess: childProcess.ChildProcess,
timeout: number, timeout: number,
preferredRevision: string preferredRevision: string,
regex = /^DevTools listening on (ws:\/\/.*)$/
): Promise<string> { ): Promise<string> {
assert(browserProcess.stderr, '`browserProcess` does not have stderr.'); assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
const rl = readline.createInterface(browserProcess.stderr); const rl = readline.createInterface(browserProcess.stderr);
@ -327,7 +348,7 @@ function waitForWSEndpoint(
function onLine(line: string): void { function onLine(line: string): void {
stderr += line + '\n'; stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); const match = line.match(regex);
if (!match) { if (!match) {
return; return;
} }

View file

@ -1,7 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {CDPBrowser} from '../common/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';
import { import {
@ -43,7 +43,7 @@ export class ChromeLauncher implements ProductLauncher {
this._isPuppeteerCore = isPuppeteerCore; this._isPuppeteerCore = isPuppeteerCore;
} }
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<CDPBrowser> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -154,7 +154,7 @@ export class ChromeLauncher implements ProductLauncher {
slowMo, slowMo,
preferredRevision: this._preferredRevision, preferredRevision: this._preferredRevision,
}); });
browser = await Browser._create( browser = await CDPBrowser._create(
this.product, this.product,
connection, connection,
[], [],

View file

@ -2,7 +2,9 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {CDPBrowser as CDPBrowser} from '../common/Browser.js';
import {Browser as BiDiBrowser} from '../common/bidi/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserFetcher} from './BrowserFetcher.js'; import {BrowserFetcher} from './BrowserFetcher.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';
@ -58,6 +60,7 @@ export class FirefoxLauncher implements ProductLauncher {
extraPrefsFirefox = {}, extraPrefsFirefox = {},
waitForInitialPage = true, waitForInitialPage = true,
debuggingPort = null, debuggingPort = null,
protocol = 'cdp',
} = options; } = options;
const firefoxArguments = []; const firefoxArguments = [];
@ -113,7 +116,9 @@ export class FirefoxLauncher implements ProductLauncher {
firefoxArguments.push(userDataDir); firefoxArguments.push(userDataDir);
} }
await this._updateRevision(); if (!this._isPuppeteerCore) {
await this._updateRevision();
}
let firefoxExecutable = executablePath; let firefoxExecutable = executablePath;
if (!executablePath) { if (!executablePath) {
const {missingText, executablePath} = resolveExecutablePath(this); const {missingText, executablePath} = resolveExecutablePath(this);
@ -143,6 +148,27 @@ export class FirefoxLauncher implements ProductLauncher {
pipe, pipe,
}); });
if (protocol === 'webDriverBiDi') {
let browser;
try {
const connection = await runner.setupWebDriverBiDiConnection({
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await BiDiBrowser.create({
connection,
closeCallback: runner.close.bind(runner),
process: runner.proc,
});
} catch (error) {
runner.kill();
throw error;
}
return browser;
}
let browser; let browser;
try { try {
const connection = await runner.setupConnection({ const connection = await runner.setupConnection({
@ -151,7 +177,7 @@ export class FirefoxLauncher implements ProductLauncher {
slowMo, slowMo,
preferredRevision: this._preferredRevision, preferredRevision: this._preferredRevision,
}); });
browser = await Browser._create( browser = await CDPBrowser._create(
this.product, this.product,
connection, connection,
[], [],

View file

@ -15,7 +15,7 @@
*/ */
import os from 'os'; import os from 'os';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {BrowserFetcher} from './BrowserFetcher.js'; import {BrowserFetcher} from './BrowserFetcher.js';
import { import {

View file

@ -22,7 +22,7 @@ import {
import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js'; import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js'; import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js';
import {BrowserConnectOptions} from '../common/BrowserConnector.js'; import {BrowserConnectOptions} from '../common/BrowserConnector.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../api/Browser.js';
import {createLauncher, ProductLauncher} from './ProductLauncher.js'; import {createLauncher, ProductLauncher} from './ProductLauncher.js';
import {PUPPETEER_REVISIONS} from '../revisions.js'; import {PUPPETEER_REVISIONS} from '../revisions.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
@ -78,6 +78,9 @@ export class PuppeteerNode extends Puppeteer {
#projectRoot?: string; #projectRoot?: string;
#productName?: Product; #productName?: Product;
/**
* @internal
*/
_preferredRevision: string; _preferredRevision: string;
/** /**

View file

@ -1,10 +1,8 @@
import {createDeferredPromise} from '../util/DeferredPromise.js'; /**
* CommonJS JavaScript code that provides the puppeteer utilities. See the
declare global { * [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md)
const InjectedUtil: { * for injection for more information.
createDeferredPromise: typeof createDeferredPromise; *
}; * @internal
} */
/** @internal */
export const source = SOURCE_CODE; export const source = SOURCE_CODE;

View file

@ -8,6 +8,5 @@
"references": [ "references": [
{"path": "../vendor/tsconfig.cjs.json"}, {"path": "../vendor/tsconfig.cjs.json"},
{"path": "../compat/cjs/tsconfig.json"} {"path": "../compat/cjs/tsconfig.json"}
], ]
"exclude": ["injected/injected.ts"]
} }

View file

@ -8,6 +8,5 @@
"references": [ "references": [
{"path": "../vendor/tsconfig.esm.json"}, {"path": "../vendor/tsconfig.esm.json"},
{"path": "../compat/esm/tsconfig.json"} {"path": "../compat/esm/tsconfig.json"}
], ]
"exclude": ["injected/injected.ts"]
} }

View file

@ -1,5 +1,6 @@
// AUTOGENERATED - Use `npm run generate:sources` to regenerate. // AUTOGENERATED - Use `npm run generate:sources` to regenerate.
export * from './api/Browser.js';
export * from './common/Accessibility.js'; export * from './common/Accessibility.js';
export * from './common/AriaQueryHandler.js'; export * from './common/AriaQueryHandler.js';
export * from './common/Browser.js'; export * from './common/Browser.js';
@ -23,11 +24,13 @@ export * from './common/FileChooser.js';
export * from './common/FirefoxTargetManager.js'; export * from './common/FirefoxTargetManager.js';
export * from './common/Frame.js'; export * from './common/Frame.js';
export * from './common/FrameManager.js'; export * from './common/FrameManager.js';
export * from './common/FrameTree.js';
export * from './common/HTTPRequest.js'; export * from './common/HTTPRequest.js';
export * from './common/HTTPResponse.js'; export * from './common/HTTPResponse.js';
export * from './common/Input.js'; export * from './common/Input.js';
export * from './common/IsolatedWorld.js'; export * from './common/IsolatedWorld.js';
export * from './common/JSHandle.js'; export * from './common/JSHandle.js';
export * from './common/LazyArg.js';
export * from './common/LifecycleWatcher.js'; export * from './common/LifecycleWatcher.js';
export * from './common/NetworkConditions.js'; export * from './common/NetworkConditions.js';
export * from './common/NetworkEventManager.js'; export * from './common/NetworkEventManager.js';
@ -47,12 +50,12 @@ export * from './common/Tracing.js';
export * from './common/types.js'; export * from './common/types.js';
export * from './common/USKeyboardLayout.js'; export * from './common/USKeyboardLayout.js';
export * from './common/util.js'; export * from './common/util.js';
export * from './common/WaitTask.js';
export * from './common/WebWorker.js'; export * from './common/WebWorker.js';
export * from './compat.d.js'; export * from './compat.d.js';
export * from './constants.js'; export * from './constants.js';
export * from './environment.js'; export * from './environment.js';
export * from './generated/injected.js'; export * from './generated/injected.js';
export * from './generated/version.js';
export * from './initializePuppeteer.js'; export * from './initializePuppeteer.js';
export * from './node/BrowserFetcher.js'; export * from './node/BrowserFetcher.js';
export * from './node/BrowserRunner.js'; export * from './node/BrowserRunner.js';

View file

@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
export interface DeferredPromise<T> extends Promise<T> { export interface DeferredPromise<T> extends Promise<T> {
finished: () => boolean; finished: () => boolean;
resolved: () => boolean; resolved: () => boolean;
resolve: (_: T) => void; resolve: (value: T) => void;
reject: (_: Error) => void; reject: (reason?: unknown) => void;
} }
/** /**
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
): DeferredPromise<T> { ): DeferredPromise<T> {
let isResolved = false; let isResolved = false;
let isRejected = false; let isRejected = false;
let resolver = (_: T): void => {}; let resolver: (value: T) => void;
let rejector = (_: Error) => {}; let rejector: (reason?: unknown) => void;
const taskPromise = new Promise<T>((resolve, reject) => { const taskPromise = new Promise<T>((resolve, reject) => {
resolver = resolve; resolver = resolve;
rejector = reject; rejector = reject;
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
isResolved = true; isResolved = true;
resolver(value); resolver(value);
}, },
reject: (err: Error) => { reject: (err?: unknown) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
isRejected = true; isRejected = true;
rejector(err); rejector(err);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
{
"testSuites": [
{
"id": "chrome-headless",
"platforms": ["linux", "win32", "darwin"],
"parameters": ["chrome", "headless"],
"expectedLineCoverage": 93
},
{
"id": "chrome-headful",
"platforms": ["linux"],
"parameters": ["chrome", "headful"],
"expectedLineCoverage": 93
},
{
"id": "chrome-new-headless",
"platforms": ["linux"],
"parameters": ["chrome", "chrome-headless"],
"expectedLineCoverage": 93
},
{
"id": "firefox-headless",
"platforms": ["linux"],
"parameters": ["firefox", "headless"],
"expectedLineCoverage": 80
},
{
"id": "firefox-bidi",
"platforms": ["linux"],
"parameters": ["firefox", "headless", "webDriverBiDi"],
"expectedLineCoverage": 56
}
],
"parameterDefinitons": {
"chrome": {
"PUPPETEER_PRODUCT": "chrome"
},
"firefox": {
"PUPPETEER_PRODUCT": "firefox"
},
"headless": {
"HEADLESS": "true"
},
"headful": {
"HEADLESS": "false"
},
"chrome-headless": {
"HEADLESS": "chrome"
},
"webDriverBiDi": {
"PUPPETEER_PROTOCOL": "webDriverBiDi"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -20,11 +20,10 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
describeChromeOnly('Target.createCDPSession', function () { describe('Target.createCDPSession', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();

View file

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import {describeChromeOnly} from './mocha-utils.js';
import expect from 'expect'; import expect from 'expect';
import { import {
NetworkManager, NetworkManager,
@ -28,9 +26,16 @@ import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
class MockCDPSession extends EventEmitter { class MockCDPSession extends EventEmitter {
async send(): Promise<any> {} async send(): Promise<any> {}
connection() {
return undefined;
}
async detach() {}
id() {
return '1';
}
} }
describeChromeOnly('NetworkManager', () => { describe('NetworkManager', () => {
it('should process extra info on multiple redirects', async () => { it('should process extra info on multiple redirects', async () => {
const mockCDPSession = new MockCDPSession(); const mockCDPSession = new MockCDPSession();
new NetworkManager(mockCDPSession, true, { new NetworkManager(mockCDPSession, true, {

View file

@ -14,24 +14,24 @@
* limitations under the License. * limitations under the License.
*/ */
import {describeChromeOnly, getTestState} from './mocha-utils'; // eslint-disable-line import/extensions import {getTestState} from './mocha-utils'; // eslint-disable-line import/extensions
import utils from './utils.js'; import utils from './utils.js';
import expect from 'expect'; import expect from 'expect';
import { import {
Browser, CDPBrowser,
BrowserContext, CDPBrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js'; } from '../../lib/cjs/puppeteer/common/Browser.js';
describeChromeOnly('TargetManager', () => { describe('TargetManager', () => {
/* We use a special browser for this test as we need the --site-per-process flag */ /* We use a special browser for this test as we need the --site-per-process flag */
let browser: Browser; let browser: CDPBrowser;
let context: BrowserContext; let context: CDPBrowserContext;
before(async () => { before(async () => {
const {puppeteer, defaultBrowserOptions} = getTestState(); const {puppeteer, defaultBrowserOptions} = getTestState();
browser = await puppeteer.launch( browser = (await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, { Object.assign({}, defaultBrowserOptions, {
args: (defaultBrowserOptions.args || []).concat([ args: (defaultBrowserOptions.args || []).concat([
'--site-per-process', '--site-per-process',
@ -39,7 +39,7 @@ describeChromeOnly('TargetManager', () => {
'--host-rules=MAP * 127.0.0.1', '--host-rules=MAP * 127.0.0.1',
]), ]),
}) })
); )) as CDPBrowser;
}); });
beforeEach(async () => { beforeEach(async () => {

View file

@ -21,10 +21,9 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeFailsFirefox,
} from './mocha-utils.js'; } from './mocha-utils.js';
describeFailsFirefox('Accessibility', function () { describe('Accessibility', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
@ -346,7 +345,7 @@ describeFailsFirefox('Accessibility', function () {
}); });
// Firefox does not support contenteditable="plaintext-only". // Firefox does not support contenteditable="plaintext-only".
describeFailsFirefox('plaintext contenteditable', function () { describe('plaintext contenteditable', function () {
it('plain text field with role should not have children', async () => { it('plain text field with role should not have children', async () => {
const {page} = getTestState(); const {page} = getTestState();

View file

@ -19,14 +19,13 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js';
import utils from './utils.js'; import utils from './utils.js';
import assert from 'assert'; import assert from 'assert';
describeChromeOnly('AriaQueryHandler', () => { describe('AriaQueryHandler', () => {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
@ -447,7 +446,7 @@ describeChromeOnly('AriaQueryHandler', () => {
let divHidden = false; let divHidden = false;
await page.setContent( await page.setContent(
`<div role='button' style='display: block;'></div>` `<div role='button' style='display: block;'>text</div>`
); );
const waitForSelector = page const waitForSelector = page
.waitForSelector('aria/[role="button"]', {hidden: true}) .waitForSelector('aria/[role="button"]', {hidden: true})
@ -469,7 +468,9 @@ describeChromeOnly('AriaQueryHandler', () => {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; let divHidden = false;
await page.setContent(`<div role='main' style='display: block;'></div>`); await page.setContent(
`<div role='main' style='display: block;'>text</div>`
);
const waitForSelector = page const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true}) .waitForSelector('aria/[role="main"]', {hidden: true})
.then(() => { .then(() => {
@ -489,7 +490,7 @@ describeChromeOnly('AriaQueryHandler', () => {
it('hidden should wait for removal', async () => { it('hidden should wait for removal', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div role='main'></div>`); await page.setContent(`<div role='main'>text</div>`);
let divRemoved = false; let divRemoved = false;
const waitForSelector = page const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true}) .waitForSelector('aria/[role="main"]', {hidden: true})
@ -517,15 +518,15 @@ describeChromeOnly('AriaQueryHandler', () => {
it('should respect timeout', async () => { it('should respect timeout', async () => {
const {page, puppeteer} = getTestState(); const {page, puppeteer} = getTestState();
let error!: Error; const error = await page
await page .waitForSelector('aria/[role="button"]', {
.waitForSelector('aria/[role="button"]', {timeout: 10}) timeout: 10,
.catch(error_ => { })
return (error = error_); .catch(error => {
return error;
}); });
expect(error).toBeTruthy();
expect(error.message).toContain( expect(error.message).toContain(
'waiting for selector `[role="button"]` failed: timeout' 'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
); );
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
}); });
@ -533,17 +534,15 @@ describeChromeOnly('AriaQueryHandler', () => {
it('should have an error message specifically for awaiting an element to be hidden', async () => { it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div role='main'></div>`); await page.setContent(`<div role='main'>text</div>`);
let error!: Error; const promise = page.waitForSelector('aria/[role="main"]', {
await page hidden: true,
.waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10}) timeout: 10,
.catch(error_ => { });
return (error = error_); await expect(promise).rejects.toMatchObject({
}); message:
expect(error).toBeTruthy(); 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
expect(error.message).toContain( });
'waiting for selector `[role="main"]` to be hidden failed: timeout'
);
}); });
it('should respond to node attribute mutation', async () => { it('should respond to node attribute mutation', async () => {
@ -582,7 +581,9 @@ describeChromeOnly('AriaQueryHandler', () => {
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => { await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error!.stack).toContain('waiting for selector `zombo` failed'); expect(error!.stack).toContain(
'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
);
}); });
}); });

View file

@ -0,0 +1,59 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import expect from 'expect';
import {Connection} from '../../../lib/cjs/puppeteer/common/bidi/Connection.js';
import {ConnectionTransport} from '../../../lib/cjs/puppeteer/common/ConnectionTransport.js';
describe('WebDriver BiDi', () => {
describe('Connection', () => {
class TestConnectionTransport implements ConnectionTransport {
sent: string[] = [];
closed = false;
send(message: string) {
this.sent.push(message);
}
close(): void {
this.closed = true;
}
}
it('should work', async () => {
const transport = new TestConnectionTransport();
const connection = new Connection(transport);
const responsePromise = connection.send('session.status', {
context: 'context',
});
expect(transport.sent).toEqual([
`{"id":1,"method":"session.status","params":{"context":"context"}}`,
]);
const id = JSON.parse(transport.sent[0]!).id;
const rawResponse = {
id,
result: {ready: false, message: 'already connected'},
};
(transport as ConnectionTransport).onmessage?.(
JSON.stringify(rawResponse)
);
const response = await responsePromise;
expect(response).toEqual(rawResponse.result);
connection.dispose();
expect(transport.closed).toBeTruthy();
});
});
});

View file

@ -15,10 +15,7 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import { import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
getTestState,
setupTestBrowserHooks,
} from './mocha-utils.js';
import {waitEvent} from './utils.js'; import {waitEvent} from './utils.js';
describe('BrowserContext', function () { describe('BrowserContext', function () {

View file

@ -19,10 +19,9 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
describeChromeOnly('Chromium-Specific Launcher tests', function () { describe('Chromium-Specific Launcher tests', function () {
describe('Puppeteer.launch |browserURL| option', function () { describe('Puppeteer.launch |browserURL| option', function () {
it('should be able to connect using browserUrl, with and without trailing slash', async () => { it('should be able to connect using browserUrl, with and without trailing slash', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
@ -138,7 +137,7 @@ describeChromeOnly('Chromium-Specific Launcher tests', function () {
}); });
}); });
describeChromeOnly('Chromium-Specific Page Tests', function () { describe('Chromium-Specific Page Tests', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
it('Page.setRequestInterception should work with intervention headers', async () => { it('Page.setRequestInterception should work with intervention headers', async () => {

View file

@ -51,24 +51,21 @@ describe('Page.click', function () {
}) })
).toBe(42); ).toBe(42);
}); });
it( it('should click the button if window.Node is removed', async () => {
'should click the button if window.Node is removed', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {
// @ts-expect-error Expected.
return delete window.Node;
});
await page.click('button');
expect(
await page.evaluate(() => { await page.evaluate(() => {
// @ts-expect-error Expected. return (globalThis as any).result;
return delete window.Node; })
}); ).toBe('Clicked');
await page.click('button'); });
expect(
await page.evaluate(() => {
return (globalThis as any).result;
})
).toBe('Clicked');
}
);
// @see https://github.com/puppeteer/puppeteer/issues/4281 // @see https://github.com/puppeteer/puppeteer/issues/4281
it('should click on a span with an inline element inside', async () => { it('should click on a span with an inline element inside', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -421,7 +418,7 @@ describe('Page.click', function () {
).toBe('Clicked'); ).toBe('Clicked');
}); });
// @see https://github.com/puppeteer/puppeteer/issues/4110 // @see https://github.com/puppeteer/puppeteer/issues/4110
xit('should click the button with fixed position inside an iframe', async () => { it.skip('should click the button with fixed position inside an iframe', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);

View file

@ -364,22 +364,19 @@ describe('Cookie specs', () => {
'At least one of the url and domain needs to be specified' 'At least one of the url and domain needs to be specified'
); );
}); });
it( it('should default to setting secure cookie for HTTPS websites', async () => {
'should default to setting secure cookie for HTTPS websites', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const SECURE_URL = 'https://example.com'; const SECURE_URL = 'https://example.com';
await page.setCookie({ await page.setCookie({
url: SECURE_URL, url: SECURE_URL,
name: 'foo', name: 'foo',
value: 'bar', value: 'bar',
}); });
const [cookie] = await page.cookies(SECURE_URL); const [cookie] = await page.cookies(SECURE_URL);
expect(cookie!.secure).toBe(true); expect(cookie!.secure).toBe(true);
} });
);
it('should be able to set unsecure cookie for HTTP website', async () => { it('should be able to set unsecure cookie for HTTP website', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -481,67 +478,64 @@ describe('Cookie specs', () => {
}, },
]); ]);
}); });
it( it('should set secure same-site cookies from a frame', async () => {
'should set secure same-site cookies from a frame', const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
async () => {
const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState();
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
...defaultBrowserOptions, ...defaultBrowserOptions,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
try {
await page.goto(httpsServer.PREFIX + '/grid.html');
await page.evaluate(src => {
let fulfill!: () => void;
const promise = new Promise<void>(x => {
return (fulfill = x);
});
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = fulfill;
iframe.src = src;
return promise;
}, httpsServer.CROSS_PROCESS_PREFIX);
await page.setCookie({
name: '127-same-site-cookie',
value: 'best',
url: httpsServer.CROSS_PROCESS_PREFIX,
sameSite: 'None',
}); });
const page = await browser.newPage(); expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
'127-same-site-cookie=best'
try { );
await page.goto(httpsServer.PREFIX + '/grid.html'); expectCookieEquals(
await page.evaluate(src => { await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
let fulfill!: () => void; [
const promise = new Promise<void>(x => { {
return (fulfill = x); name: '127-same-site-cookie',
}); value: 'best',
const iframe = document.createElement('iframe'); domain: '127.0.0.1',
document.body.appendChild(iframe); path: '/',
iframe.onload = fulfill; sameParty: false,
iframe.src = src; expires: -1,
return promise; size: 24,
}, httpsServer.CROSS_PROCESS_PREFIX); httpOnly: false,
await page.setCookie({ sameSite: 'None',
name: '127-same-site-cookie', secure: true,
value: 'best', session: true,
url: httpsServer.CROSS_PROCESS_PREFIX, sourcePort: 443,
sameSite: 'None', sourceScheme: 'Secure',
}); },
]
expect(await page.frames()[1]!.evaluate('document.cookie')).toBe( );
'127-same-site-cookie=best' } finally {
); await page.close();
expectCookieEquals( await browser.close();
await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
[
{
name: '127-same-site-cookie',
value: 'best',
domain: '127.0.0.1',
path: '/',
sameParty: false,
expires: -1,
size: 24,
httpOnly: false,
sameSite: 'None',
secure: true,
session: true,
sourcePort: 443,
sourceScheme: 'Secure',
},
]
);
} finally {
await page.close();
await browser.close();
}
} }
); });
}); });
describe('Page.deleteCookie', function () { describe('Page.deleteCookie', function () {

View file

@ -19,11 +19,10 @@ import {
getTestState, getTestState,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
setupTestBrowserHooks, setupTestBrowserHooks,
describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
describe('Coverage specs', function () { describe('Coverage specs', function () {
describeChromeOnly('JSCoverage', function () { describe('JSCoverage', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
@ -134,7 +133,7 @@ describe('Coverage specs', function () {
).toBeGolden('jscoverage-involved.txt'); ).toBeGolden('jscoverage-involved.txt');
}); });
// @see https://crbug.com/990945 // @see https://crbug.com/990945
xit('should not hang when there is a debugger statement', async () => { it.skip('should not hang when there is a debugger statement', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.coverage.startJSCoverage(); await page.coverage.startJSCoverage();
@ -190,7 +189,7 @@ describe('Coverage specs', function () {
}); });
}); });
// @see https://crbug.com/990945 // @see https://crbug.com/990945
xit('should not hang when there is a debugger statement', async () => { it.skip('should not hang when there is a debugger statement', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.coverage.startJSCoverage(); await page.coverage.startJSCoverage();
@ -202,7 +201,7 @@ describe('Coverage specs', function () {
}); });
}); });
describeChromeOnly('CSSCoverage', function () { describe('CSSCoverage', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();

View file

@ -19,10 +19,9 @@ import {
getTestState, getTestState,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
setupTestBrowserHooks, setupTestBrowserHooks,
describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
describeChromeOnly('Input.drag', function () { describe('Input.drag', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
it('should throw an exception if not enabled before usage', async () => { it('should throw an exception if not enabled before usage', async () => {

View file

@ -37,7 +37,7 @@ describe('Evaluation specs', function () {
}); });
expect(result).toBe(21); expect(result).toBe(21);
}); });
(bigint ? it : xit)('should transfer BigInt', async () => { (bigint ? it : it.skip)('should transfer BigInt', async () => {
const {page} = getTestState(); const {page} = getTestState();
const result = await page.evaluate((a: bigint) => { const result = await page.evaluate((a: bigint) => {
@ -113,18 +113,15 @@ describe('Evaluation specs', function () {
await page.goto(server.PREFIX + '/global-var.html'); await page.goto(server.PREFIX + '/global-var.html');
expect(await page.evaluate('globalVar')).toBe(123); expect(await page.evaluate('globalVar')).toBe(123);
}); });
it( it('should return undefined for objects with symbols', async () => {
'should return undefined for objects with symbols', const {page} = getTestState();
async () => {
const {page} = getTestState();
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return [Symbol('foo4')]; return [Symbol('foo4')];
}) })
).toBe(undefined); ).toBe(undefined);
} });
);
it('should work with function shorthands', async () => { it('should work with function shorthands', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -261,7 +258,7 @@ describe('Evaluation specs', function () {
expect(result).not.toBe(object); expect(result).not.toBe(object);
expect(result).toEqual(object); expect(result).toEqual(object);
}); });
(bigint ? it : xit)('should return BigInt', async () => { (bigint ? it : it.skip)('should return BigInt', async () => {
const {page} = getTestState(); const {page} = getTestState();
const result = await page.evaluate(() => { const result = await page.evaluate(() => {
@ -322,18 +319,15 @@ describe('Evaluation specs', function () {
}) })
).toEqual({}); ).toEqual({});
}); });
it( it('should return undefined for non-serializable objects', async () => {
'should return undefined for non-serializable objects', const {page} = getTestState();
async () => {
const {page} = getTestState();
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return window; return window;
}) })
).toBe(undefined); ).toBe(undefined);
} });
);
it('should fail for circular object', async () => { it('should fail for circular object', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -408,27 +402,24 @@ describe('Evaluation specs', function () {
}); });
expect(error.message).toContain('JSHandle is disposed'); expect(error.message).toContain('JSHandle is disposed');
}); });
it( it('should throw if elementHandles are from other frames', async () => {
'should throw if elementHandles are from other frames', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const bodyHandle = await page.frames()[1]!.$('body'); const bodyHandle = await page.frames()[1]!.$('body');
let error!: Error; let error!: Error;
await page await page
.evaluate(body => { .evaluate(body => {
return body?.innerHTML; return body?.innerHTML;
}, bodyHandle) }, bodyHandle)
.catch(error_ => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain( expect(error.message).toContain(
'JSHandles can be evaluated only in the context they were created' 'JSHandles can be evaluated only in the context they were created'
); );
} });
);
it('should simulate a user gesture', async () => { it('should simulate a user gesture', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -459,19 +450,16 @@ describe('Evaluation specs', function () {
}); });
expect((error as Error).message).toContain('navigation'); expect((error as Error).message).toContain('navigation');
}); });
it( it('should not throw an error when evaluation does a navigation', async () => {
'should not throw an error when evaluation does a navigation', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/one-style.html'); await page.goto(server.PREFIX + '/one-style.html');
const result = await page.evaluate(() => { const result = await page.evaluate(() => {
(window as any).location = '/empty.html'; (window as any).location = '/empty.html';
return [42]; return [42];
}); });
expect(result).toEqual([42]); expect(result).toEqual([42]);
} });
);
it('should transfer 100Mb of data from page to node.js', async function () { it('should transfer 100Mb of data from page to node.js', async function () {
const {page} = getTestState(); const {page} = getTestState();

View file

@ -17,12 +17,12 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import expect from 'expect'; import expect from 'expect';
import {getTestState, itHeadlessOnly} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';
import path from 'path'; import path from 'path';
describe('Fixtures', function () { describe('Fixtures', function () {
itHeadlessOnly('dumpio option should work with pipe option', async () => { it('dumpio option should work with pipe option', async () => {
const {defaultBrowserOptions, puppeteerPath, headless} = getTestState(); const {defaultBrowserOptions, puppeteerPath, headless} = getTestState();
if (headless === 'chrome') { if (headless === 'chrome') {
// This test only works in the old headless mode. // This test only works in the old headless mode.

View file

@ -137,40 +137,37 @@ describe('Frame specs', function () {
' http://localhost:<PORT>/frames/frame.html (aframe)', ' http://localhost:<PORT>/frames/frame.html (aframe)',
]); ]);
}); });
it( it('should send events when frames are manipulated dynamically', async () => {
'should send events when frames are manipulated dynamically', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
// validate frameattached events // validate frameattached events
const attachedFrames: Frame[] = []; const attachedFrames: Frame[] = [];
page.on('frameattached', frame => { page.on('frameattached', frame => {
return attachedFrames.push(frame); return attachedFrames.push(frame);
}); });
await utils.attachFrame(page, 'frame1', './assets/frame.html'); await utils.attachFrame(page, 'frame1', './assets/frame.html');
expect(attachedFrames.length).toBe(1); expect(attachedFrames.length).toBe(1);
expect(attachedFrames[0]!.url()).toContain('/assets/frame.html'); expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
// validate framenavigated events // validate framenavigated events
const navigatedFrames: Frame[] = []; const navigatedFrames: Frame[] = [];
page.on('framenavigated', frame => { page.on('framenavigated', frame => {
return navigatedFrames.push(frame); return navigatedFrames.push(frame);
}); });
await utils.navigateFrame(page, 'frame1', './empty.html'); await utils.navigateFrame(page, 'frame1', './empty.html');
expect(navigatedFrames.length).toBe(1); expect(navigatedFrames.length).toBe(1);
expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE); expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
// validate framedetached events // validate framedetached events
const detachedFrames: Frame[] = []; const detachedFrames: Frame[] = [];
page.on('framedetached', frame => { page.on('framedetached', frame => {
return detachedFrames.push(frame); return detachedFrames.push(frame);
}); });
await utils.detachFrame(page, 'frame1'); await utils.detachFrame(page, 'frame1');
expect(detachedFrames.length).toBe(1); expect(detachedFrames.length).toBe(1);
expect(detachedFrames[0]!.isDetached()).toBe(true); expect(detachedFrames[0]!.isDetached()).toBe(true);
} });
);
it('should send "framenavigated" when navigating on anchor URLs', async () => { it('should send "framenavigated" when navigating on anchor URLs', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -299,31 +296,24 @@ describe('Frame specs', function () {
expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame()); expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame()); expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
}); });
it( it('should report different frame instance when frame re-attaches', async () => {
'should report different frame instance when frame re-attaches', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
const frame1 = await utils.attachFrame( const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
page, await page.evaluate(() => {
'frame1', (globalThis as any).frame = document.querySelector('#frame1');
server.EMPTY_PAGE (globalThis as any).frame.remove();
); });
await page.evaluate(() => { expect(frame1!.isDetached()).toBe(true);
(globalThis as any).frame = document.querySelector('#frame1'); const [frame2] = await Promise.all([
(globalThis as any).frame.remove(); utils.waitEvent(page, 'frameattached'),
}); page.evaluate(() => {
expect(frame1!.isDetached()).toBe(true); return document.body.appendChild((globalThis as any).frame);
const [frame2] = await Promise.all([ }),
utils.waitEvent(page, 'frameattached'), ]);
page.evaluate(() => { expect(frame2.isDetached()).toBe(false);
return document.body.appendChild((globalThis as any).frame); expect(frame1).not.toBe(frame2);
}), });
]);
expect(frame2.isDetached()).toBe(false);
expect(frame1).not.toBe(frame2);
}
);
it('should support url fragment', async () => { it('should support url fragment', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();

View file

@ -24,11 +24,7 @@ import {
PuppeteerLaunchOptions, PuppeteerLaunchOptions,
PuppeteerNode, PuppeteerNode,
} from '../../lib/cjs/puppeteer/node/Puppeteer.js'; } from '../../lib/cjs/puppeteer/node/Puppeteer.js';
import { import {getTestState} from './mocha-utils.js';
describeChromeOnly,
getTestState,
itFailsWindows,
} from './mocha-utils.js';
const rmAsync = promisify(rimraf); const rmAsync = promisify(rimraf);
const mkdtempAsync = promisify(fs.mkdtemp); const mkdtempAsync = promisify(fs.mkdtemp);
@ -44,7 +40,7 @@ const serviceWorkerExtensionPath = path.join(
'extension' 'extension'
); );
describeChromeOnly('headful tests', function () { describe('headful tests', function () {
/* These tests fire up an actual browser so let's /* These tests fire up an actual browser so let's
* allow a higher timeout * allow a higher timeout
*/ */
@ -214,43 +210,40 @@ describeChromeOnly('headful tests', function () {
expect(pages).toEqual(['about:blank']); expect(pages).toEqual(['about:blank']);
await browser.close(); await browser.close();
}); });
itFailsWindows( it('headless should be able to read cookies written by headful', async () => {
'headless should be able to read cookies written by headful', /* Needs investigation into why but this fails consistently on Windows CI. */
async () => { const {server, puppeteer} = getTestState();
/* Needs investigation into why but this fails consistently on Windows CI. */
const {server, puppeteer} = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER); const userDataDir = await mkdtempAsync(TMP_FOLDER);
// Write a cookie in headful chrome // Write a cookie in headful chrome
const headfulBrowser = await launchBrowser( const headfulBrowser = await launchBrowser(
puppeteer, puppeteer,
Object.assign({userDataDir}, headfulOptions) Object.assign({userDataDir}, headfulOptions)
); );
const headfulPage = await headfulBrowser.newPage(); const headfulPage = await headfulBrowser.newPage();
await headfulPage.goto(server.EMPTY_PAGE); await headfulPage.goto(server.EMPTY_PAGE);
await headfulPage.evaluate(() => { await headfulPage.evaluate(() => {
return (document.cookie = return (document.cookie =
'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
}); });
await headfulBrowser.close(); await headfulBrowser.close();
// Read the cookie from headless chrome // Read the cookie from headless chrome
const headlessBrowser = await launchBrowser( const headlessBrowser = await launchBrowser(
puppeteer, puppeteer,
Object.assign({userDataDir}, headlessOptions) Object.assign({userDataDir}, headlessOptions)
); );
const headlessPage = await headlessBrowser.newPage(); const headlessPage = await headlessBrowser.newPage();
await headlessPage.goto(server.EMPTY_PAGE); await headlessPage.goto(server.EMPTY_PAGE);
const cookie = await headlessPage.evaluate(() => { const cookie = await headlessPage.evaluate(() => {
return document.cookie; return document.cookie;
}); });
await headlessBrowser.close(); await headlessBrowser.close();
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(() => {}); await rmAsync(userDataDir).catch(() => {});
expect(cookie).toBe('foo=true'); expect(cookie).toBe('foo=true');
} });
);
// TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
xit('OOPIF: should report google.com frame', async () => { it.skip('OOPIF: should report google.com frame', async () => {
const {server, puppeteer} = getTestState(); const {server, puppeteer} = getTestState();
// https://google.com is isolated by default in Chromium embedder. // https://google.com is isolated by default in Chromium embedder.

View file

@ -16,15 +16,10 @@
import expect from 'expect'; import expect from 'expect';
import {TLSSocket} from 'tls'; import {TLSSocket} from 'tls';
import { import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
import { import {getTestState} from './mocha-utils.js';
getTestState
} from './mocha-utils.js';
describe('ignoreHTTPSErrors', function () { describe('ignoreHTTPSErrors', function () {
/* Note that this test creates its own browser rather than use /* Note that this test creates its own browser rather than use

View file

@ -23,18 +23,35 @@ import {
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
describe('InjectedUtil tests', function () { describe('PuppeteerUtil tests', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
it('should work', async () => { it('should work', async () => {
const {page} = getTestState(); const {page} = getTestState();
const handle = await page const world = page.mainFrame().worlds[PUPPETEER_WORLD];
.mainFrame() const value = await world.evaluate(PuppeteerUtil => {
.worlds[PUPPETEER_WORLD].evaluate(() => { return typeof PuppeteerUtil === 'object';
return typeof InjectedUtil === 'object'; }, world.puppeteerUtil);
}); expect(value).toBeTruthy();
expect(handle).toBeTruthy(); });
describe('createFunction tests', function () {
it('should work', async () => {
const {page} = getTestState();
const world = page.mainFrame().worlds[PUPPETEER_WORLD];
const value = await world.evaluate(
({createFunction}, fnString) => {
return createFunction(fnString)(4);
},
await world.puppeteerUtil,
(() => {
return 4;
}).toString()
);
expect(value).toBe(4);
});
}); });
}); });

View file

@ -20,7 +20,6 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeFailsFirefox,
} from './mocha-utils.js'; } from './mocha-utils.js';
const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt'); const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
@ -29,7 +28,7 @@ describe('input tests', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
describeFailsFirefox('input', function () { describe('input', function () {
it('should upload the file', async () => { it('should upload the file', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -76,7 +75,7 @@ describe('input tests', function () {
}); });
}); });
describeFailsFirefox('Page.waitForFileChooser', function () { describe('Page.waitForFileChooser', function () {
it('should work when file input is attached to DOM', async () => { it('should work when file input is attached to DOM', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -159,7 +158,7 @@ describe('input tests', function () {
}); });
}); });
describeFailsFirefox('FileChooser.accept', function () { describe('FileChooser.accept', function () {
it('should accept single file', async () => { it('should accept single file', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -325,7 +324,7 @@ describe('input tests', function () {
}); });
}); });
describeFailsFirefox('FileChooser.cancel', function () { describe('FileChooser.cancel', function () {
it('should cancel dialog', async () => { it('should cancel dialog', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -373,7 +372,7 @@ describe('input tests', function () {
}); });
}); });
describeFailsFirefox('FileChooser.isMultiple', () => { describe('FileChooser.isMultiple', () => {
it('should work for single file pick', async () => { it('should work for single file pick', async () => {
const {page} = getTestState(); const {page} = getTestState();

View file

@ -119,21 +119,18 @@ describe('Keyboard', function () {
}) })
).toBe('a'); ).toBe('a');
}); });
it( it('ElementHandle.press should support |text| option', async () => {
'ElementHandle.press should support |text| option', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
const textarea = (await page.$('textarea'))!; const textarea = (await page.$('textarea'))!;
await textarea.press('a', {text: 'ё'}); await textarea.press('a', {text: 'ё'});
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return document.querySelector('textarea')!.value; return document.querySelector('textarea')!.value;
}) })
).toBe('ё'); ).toBe('ё');
} });
);
it('should send a character with sendCharacter', async () => { it('should send a character with sendCharacter', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();

View file

@ -24,12 +24,7 @@ import {TLSSocket} from 'tls';
import {promisify} from 'util'; import {promisify} from 'util';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {Product} from '../../lib/cjs/puppeteer/common/Product.js'; import {Product} from '../../lib/cjs/puppeteer/common/Product.js';
import { import {getTestState, itOnlyRegularInstall} from './mocha-utils.js';
getTestState,
itChromeOnly,
itFirefoxOnly,
itOnlyRegularInstall,
} from './mocha-utils.js';
import utils from './utils.js'; import utils from './utils.js';
const mkdtempAsync = promisify(fs.mkdtemp); const mkdtempAsync = promisify(fs.mkdtemp);
@ -208,6 +203,11 @@ describe('Launcher specs', function () {
}); });
}); });
describe('Puppeteer.launch', function () { describe('Puppeteer.launch', function () {
it('can launch and close the browser', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions);
await browser.close();
});
it('should reject all promises when browser is closed', async () => { it('should reject all promises when browser is closed', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions); const browser = await puppeteer.launch(defaultBrowserOptions);
@ -250,7 +250,7 @@ describe('Launcher specs', function () {
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(() => {}); await rmAsync(userDataDir).catch(() => {});
}); });
itChromeOnly('tmp profile should be cleaned up', async () => { it('tmp profile should be cleaned up', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
// Set a custom test tmp dir so that we can validate that // Set a custom test tmp dir so that we can validate that
@ -279,7 +279,7 @@ describe('Launcher specs', function () {
// Restore env var // Restore env var
process.env['PUPPETEER_TMP_DIR'] = ''; process.env['PUPPETEER_TMP_DIR'] = '';
}); });
itFirefoxOnly('userDataDir option restores preferences', async () => { it('userDataDir option restores preferences', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER); const userDataDir = await mkdtempAsync(TMP_FOLDER);
@ -325,7 +325,7 @@ describe('Launcher specs', function () {
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(() => {}); await rmAsync(userDataDir).catch(() => {});
}); });
itChromeOnly('userDataDir argument with non-existent dir', async () => { it('userDataDir argument with non-existent dir', async () => {
const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); const {isChrome, puppeteer, defaultBrowserOptions} = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER); const userDataDir = await mkdtempAsync(TMP_FOLDER);
@ -459,49 +459,43 @@ describe('Launcher specs', function () {
await page.close(); await page.close();
await browser.close(); await browser.close();
}); });
itChromeOnly( it('should filter out ignored default arguments in Chrome', async () => {
'should filter out ignored default arguments in Chrome', const {defaultBrowserOptions, puppeteer} = getTestState();
async () => { // Make sure we launch with `--enable-automation` by default.
const {defaultBrowserOptions, puppeteer} = getTestState(); const defaultArgs = puppeteer.defaultArgs();
// Make sure we launch with `--enable-automation` by default. const browser = await puppeteer.launch(
const defaultArgs = puppeteer.defaultArgs(); Object.assign({}, defaultBrowserOptions, {
const browser = await puppeteer.launch( // Ignore first and third default argument.
Object.assign({}, defaultBrowserOptions, { ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
// Ignore first and third default argument. })
ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]], );
}) const spawnargs = browser.process()!.spawnargs;
); if (!spawnargs) {
const spawnargs = browser.process()!.spawnargs; throw new Error('spawnargs not present');
if (!spawnargs) {
throw new Error('spawnargs not present');
}
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
await browser.close();
} }
); expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
itFirefoxOnly( expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
'should filter out ignored default argument in Firefox', expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
async () => { await browser.close();
const {defaultBrowserOptions, puppeteer} = getTestState(); });
it('should filter out ignored default argument in Firefox', async () => {
const {defaultBrowserOptions, puppeteer} = getTestState();
const defaultArgs = puppeteer.defaultArgs(); const defaultArgs = puppeteer.defaultArgs();
const browser = await puppeteer.launch( const browser = await puppeteer.launch(
Object.assign({}, defaultBrowserOptions, { Object.assign({}, defaultBrowserOptions, {
// Only the first argument is fixed, others are optional. // Only the first argument is fixed, others are optional.
ignoreDefaultArgs: [defaultArgs[0]!], ignoreDefaultArgs: [defaultArgs[0]!],
}) })
); );
const spawnargs = browser.process()!.spawnargs; const spawnargs = browser.process()!.spawnargs;
if (!spawnargs) { if (!spawnargs) {
throw new Error('spawnargs not present'); throw new Error('spawnargs not present');
}
expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
await browser.close();
} }
); expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
await browser.close();
});
it('should have default URL when launching browser', async function () { it('should have default URL when launching browser', async function () {
const {defaultBrowserOptions, puppeteer} = getTestState(); const {defaultBrowserOptions, puppeteer} = getTestState();
const browser = await puppeteer.launch(defaultBrowserOptions); const browser = await puppeteer.launch(defaultBrowserOptions);
@ -511,24 +505,21 @@ describe('Launcher specs', function () {
expect(pages).toEqual(['about:blank']); expect(pages).toEqual(['about:blank']);
await browser.close(); await browser.close();
}); });
it( it('should have custom URL when launching browser', async () => {
'should have custom URL when launching browser', const {server, puppeteer, defaultBrowserOptions} = getTestState();
async () => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
const options = Object.assign({}, defaultBrowserOptions); const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []); options.args = [server.EMPTY_PAGE].concat(options.args || []);
const browser = await puppeteer.launch(options); const browser = await puppeteer.launch(options);
const pages = await browser.pages(); const pages = await browser.pages();
expect(pages.length).toBe(1); expect(pages.length).toBe(1);
const page = pages[0]!; const page = pages[0]!;
if (page.url() !== server.EMPTY_PAGE) { if (page.url() !== server.EMPTY_PAGE) {
await page.waitForNavigation(); await page.waitForNavigation();
}
expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
} }
); expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close();
});
it('should pass the timeout parameter to browser.waitForTarget', async () => { it('should pass the timeout parameter to browser.waitForTarget', async () => {
const {puppeteer, defaultBrowserOptions} = getTestState(); const {puppeteer, defaultBrowserOptions} = getTestState();
const options = Object.assign({}, defaultBrowserOptions, { const options = Object.assign({}, defaultBrowserOptions, {
@ -614,24 +605,21 @@ describe('Launcher specs', function () {
}); });
expect(error.message).toContain('either pipe or debugging port'); expect(error.message).toContain('either pipe or debugging port');
}); });
itChromeOnly( it('should launch Chrome properly with --no-startup-window and waitForInitialPage=false', async () => {
'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', const {defaultBrowserOptions, puppeteer} = getTestState();
async () => { const options = {
const {defaultBrowserOptions, puppeteer} = getTestState(); waitForInitialPage: false,
const options = { // This is needed to prevent Puppeteer from adding an initial blank page.
waitForInitialPage: false, // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
// This is needed to prevent Puppeteer from adding an initial blank page. ignoreDefaultArgs: true,
// See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200 ...defaultBrowserOptions,
ignoreDefaultArgs: true, args: ['--no-startup-window'],
...defaultBrowserOptions, };
args: ['--no-startup-window'], const browser = await puppeteer.launch(options);
}; const pages = await browser.pages();
const browser = await puppeteer.launch(options); expect(pages.length).toBe(0);
const pages = await browser.pages(); await browser.close();
expect(pages.length).toBe(0); });
await browser.close();
}
);
}); });
describe('Puppeteer.launch', function () { describe('Puppeteer.launch', function () {
@ -808,68 +796,62 @@ describe('Launcher specs', function () {
.sort() .sort()
).toEqual(['about:blank', server.EMPTY_PAGE]); ).toEqual(['about:blank', server.EMPTY_PAGE]);
}); });
it( it('should be able to reconnect to a disconnected browser', async () => {
'should be able to reconnect to a disconnected browser', const {server, puppeteer, defaultBrowserOptions} = getTestState();
async () => {
const {server, puppeteer, defaultBrowserOptions} = getTestState();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions); const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint(); const browserWSEndpoint = originalBrowser.wsEndpoint();
const page = await originalBrowser.newPage(); const page = await originalBrowser.newPage();
await page.goto(server.PREFIX + '/frames/nested-frames.html'); await page.goto(server.PREFIX + '/frames/nested-frames.html');
originalBrowser.disconnect(); originalBrowser.disconnect();
const browser = await puppeteer.connect({browserWSEndpoint}); const browser = await puppeteer.connect({browserWSEndpoint});
const pages = await browser.pages(); const pages = await browser.pages();
const restoredPage = pages.find(page => { const restoredPage = pages.find(page => {
return page.url() === server.PREFIX + '/frames/nested-frames.html'; return page.url() === server.PREFIX + '/frames/nested-frames.html';
})!; })!;
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
'http://localhost:<PORT>/frames/nested-frames.html', 'http://localhost:<PORT>/frames/nested-frames.html',
' http://localhost:<PORT>/frames/two-frames.html (2frames)', ' http://localhost:<PORT>/frames/two-frames.html (2frames)',
' http://localhost:<PORT>/frames/frame.html (uno)', ' http://localhost:<PORT>/frames/frame.html (uno)',
' http://localhost:<PORT>/frames/frame.html (dos)', ' http://localhost:<PORT>/frames/frame.html (dos)',
' http://localhost:<PORT>/frames/frame.html (aframe)', ' http://localhost:<PORT>/frames/frame.html (aframe)',
]); ]);
expect( expect(
await restoredPage.evaluate(() => { await restoredPage.evaluate(() => {
return 7 * 8; return 7 * 8;
}) })
).toBe(56); ).toBe(56);
await browser.close(); await browser.close();
} });
);
// @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
it( it('should be able to connect to the same page simultaneously', async () => {
'should be able to connect to the same page simultaneously', const {puppeteer, defaultBrowserOptions} = getTestState();
async () => {
const {puppeteer, defaultBrowserOptions} = getTestState();
const browserOne = await puppeteer.launch(defaultBrowserOptions); const browserOne = await puppeteer.launch(defaultBrowserOptions);
const browserTwo = await puppeteer.connect({ const browserTwo = await puppeteer.connect({
browserWSEndpoint: browserOne.wsEndpoint(), browserWSEndpoint: browserOne.wsEndpoint(),
}); });
const [page1, page2] = await Promise.all([ const [page1, page2] = await Promise.all([
new Promise<Page>(x => { new Promise<Page>(x => {
return browserOne.once('targetcreated', target => { return browserOne.once('targetcreated', target => {
return x(target.page()); return x(target.page());
}); });
}), }),
browserTwo.newPage(), browserTwo.newPage(),
]); ]);
expect( expect(
await page1.evaluate(() => { await page1.evaluate(() => {
return 7 * 8; return 7 * 8;
}) })
).toBe(56); ).toBe(56);
expect( expect(
await page2.evaluate(() => { await page2.evaluate(() => {
return 7 * 6; return 7 * 6;
}) })
).toBe(42); ).toBe(42);
await browserOne.close(); await browserOne.close();
} });
);
it('should be able to reconnect', async () => { it('should be able to reconnect', async () => {
const {puppeteer, server, defaultBrowserOptions} = getTestState(); const {puppeteer, server, defaultBrowserOptions} = getTestState();
const browserOne = await puppeteer.launch(defaultBrowserOptions); const browserOne = await puppeteer.launch(defaultBrowserOptions);
@ -932,7 +914,7 @@ describe('Launcher specs', function () {
describe('when the product is chrome, platform is not darwin, and arch is arm64', () => { describe('when the product is chrome, platform is not darwin, and arch is arm64', () => {
describe('and the executable exists', () => { describe('and the executable exists', () => {
itChromeOnly('returns /usr/bin/chromium-browser', async () => { it('returns /usr/bin/chromium-browser', async () => {
const {puppeteer} = getTestState(); const {puppeteer} = getTestState();
const osPlatformStub = sinon.stub(os, 'platform').returns('linux'); const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
const osArchStub = sinon.stub(os, 'arch').returns('arm64'); const osArchStub = sinon.stub(os, 'arch').returns('arm64');
@ -971,26 +953,21 @@ describe('Launcher specs', function () {
}); });
}); });
describe('and the executable does not exist', () => { describe('and the executable does not exist', () => {
itChromeOnly( it('does not return /usr/bin/chromium-browser', async () => {
'does not return /usr/bin/chromium-browser', const {puppeteer} = getTestState();
async () => { const osPlatformStub = sinon.stub(os, 'platform').returns('linux');
const {puppeteer} = getTestState(); const osArchStub = sinon.stub(os, 'arch').returns('arm64');
const osPlatformStub = sinon const fsExistsStub = sinon.stub(fs, 'existsSync');
.stub(os, 'platform') fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false);
.returns('linux');
const osArchStub = sinon.stub(os, 'arch').returns('arm64');
const fsExistsStub = sinon.stub(fs, 'existsSync');
fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false);
const executablePath = puppeteer.executablePath(); const executablePath = puppeteer.executablePath();
expect(executablePath).not.toEqual('/usr/bin/chromium-browser'); expect(executablePath).not.toEqual('/usr/bin/chromium-browser');
osPlatformStub.restore(); osPlatformStub.restore();
osArchStub.restore(); osArchStub.restore();
fsExistsStub.restore(); fsExistsStub.restore();
} });
);
}); });
}); });
}); });
@ -1020,51 +997,48 @@ describe('Launcher specs', function () {
}); });
describe('Browser.Events.disconnected', function () { describe('Browser.Events.disconnected', function () {
it( it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
'should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', const {puppeteer, defaultBrowserOptions} = getTestState();
async () => { const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const {puppeteer, defaultBrowserOptions} = getTestState(); const browserWSEndpoint = originalBrowser.wsEndpoint();
const originalBrowser = await puppeteer.launch(defaultBrowserOptions); const remoteBrowser1 = await puppeteer.connect({
const browserWSEndpoint = originalBrowser.wsEndpoint(); browserWSEndpoint,
const remoteBrowser1 = await puppeteer.connect({ });
browserWSEndpoint, const remoteBrowser2 = await puppeteer.connect({
}); browserWSEndpoint,
const remoteBrowser2 = await puppeteer.connect({ });
browserWSEndpoint,
});
let disconnectedOriginal = 0; let disconnectedOriginal = 0;
let disconnectedRemote1 = 0; let disconnectedRemote1 = 0;
let disconnectedRemote2 = 0; let disconnectedRemote2 = 0;
originalBrowser.on('disconnected', () => { originalBrowser.on('disconnected', () => {
return ++disconnectedOriginal; return ++disconnectedOriginal;
}); });
remoteBrowser1.on('disconnected', () => { remoteBrowser1.on('disconnected', () => {
return ++disconnectedRemote1; return ++disconnectedRemote1;
}); });
remoteBrowser2.on('disconnected', () => { remoteBrowser2.on('disconnected', () => {
return ++disconnectedRemote2; return ++disconnectedRemote2;
}); });
await Promise.all([ await Promise.all([
utils.waitEvent(remoteBrowser2, 'disconnected'), utils.waitEvent(remoteBrowser2, 'disconnected'),
remoteBrowser2.disconnect(), remoteBrowser2.disconnect(),
]); ]);
expect(disconnectedOriginal).toBe(0); expect(disconnectedOriginal).toBe(0);
expect(disconnectedRemote1).toBe(0); expect(disconnectedRemote1).toBe(0);
expect(disconnectedRemote2).toBe(1); expect(disconnectedRemote2).toBe(1);
await Promise.all([ await Promise.all([
utils.waitEvent(remoteBrowser1, 'disconnected'), utils.waitEvent(remoteBrowser1, 'disconnected'),
utils.waitEvent(originalBrowser, 'disconnected'), utils.waitEvent(originalBrowser, 'disconnected'),
originalBrowser.close(), originalBrowser.close(),
]); ]);
expect(disconnectedOriginal).toBe(1); expect(disconnectedOriginal).toBe(1);
expect(disconnectedRemote1).toBe(1); expect(disconnectedRemote1).toBe(1);
expect(disconnectedRemote2).toBe(1); expect(disconnectedRemote2).toBe(1);
} });
);
}); });
}); });

View file

@ -17,14 +17,10 @@
import Protocol from 'devtools-protocol'; import Protocol from 'devtools-protocol';
import expect from 'expect'; import expect from 'expect';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import sinon from 'sinon'; import sinon from 'sinon';
import { import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
@ -34,6 +30,7 @@ import {
import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js'; import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js';
import {TestServer} from '../../utils/testserver/lib/index.js'; import {TestServer} from '../../utils/testserver/lib/index.js';
import {extendExpectWithToBeGolden} from './utils.js'; import {extendExpectWithToBeGolden} from './utils.js';
import * as Mocha from 'mocha';
const setupServer = async () => { const setupServer = async () => {
const assetsPath = path.join(__dirname, '../assets'); const assetsPath = path.join(__dirname, '../assets');
@ -63,14 +60,15 @@ export const getTestState = (): PuppeteerTestState => {
}; };
const product = const product =
process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'Chromium'; process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome';
const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false; const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false;
const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase(); const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
const isHeadless = headless === 'true' || headless === 'chrome'; const isHeadless = headless === 'true' || headless === 'chrome';
const isFirefox = product === 'firefox'; const isFirefox = product === 'firefox';
const isChrome = product === 'Chromium'; const isChrome = product === 'chrome';
const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp';
let extraLaunchOptions = {}; let extraLaunchOptions = {};
try { try {
@ -91,6 +89,7 @@ const defaultBrowserOptions = Object.assign(
executablePath: process.env['BINARY'], executablePath: process.env['BINARY'],
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless, headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
dumpio: !!process.env['DUMPIO'], dumpio: !!process.env['DUMPIO'],
protocol: protocol as 'cdp' | 'webDriverBiDi',
}, },
extraLaunchOptions extraLaunchOptions
); );
@ -125,7 +124,11 @@ declare module 'expect/build/types' {
} }
const setupGoldenAssertions = (): void => { const setupGoldenAssertions = (): void => {
const suffix = product.toLowerCase(); let suffix = product.toLowerCase();
if (suffix === 'chrome') {
// TODO: to avoid moving golden folders.
suffix = 'chromium';
}
const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`); const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`); const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
if (fs.existsSync(OUTPUT_DIR)) { if (fs.existsSync(OUTPUT_DIR)) {
@ -152,116 +155,21 @@ interface PuppeteerTestState {
} }
const state: Partial<PuppeteerTestState> = {}; const state: Partial<PuppeteerTestState> = {};
export const itFailsFirefox = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (isFirefox) {
return xit(description, body);
} else {
return it(description, body);
}
};
export const itChromeOnly = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (isChrome) {
return it(description, body);
} else {
return xit(description, body);
}
};
export const itHeadlessOnly = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (isChrome && isHeadless === true) {
return it(description, body);
} else {
return xit(description, body);
}
};
export const itHeadfulOnly = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (isChrome && isHeadless === false) {
return it(description, body);
} else {
return xit(description, body);
}
};
export const itFirefoxOnly = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (isFirefox) {
return it(description, body);
} else {
return xit(description, body);
}
};
export const itOnlyRegularInstall = ( export const itOnlyRegularInstall = (
description: string, description: string,
body: Mocha.Func body: Mocha.AsyncFunc
): Mocha.Test => { ): Mocha.Test => {
if (alternativeInstall || process.env['BINARY']) { if (alternativeInstall || process.env['BINARY']) {
return xit(description, body); return it.skip(description, body);
} else { } else {
return it(description, body); return it(description, body);
} }
}; };
export const itFailsWindowsUntilDate = ( if (
date: Date, process.env['MOCHA_WORKER_ID'] === undefined ||
description: string, process.env['MOCHA_WORKER_ID'] === '0'
body: Mocha.Func ) {
): Mocha.Test => {
if (os.platform() === 'win32' && Date.now() < date.getTime()) {
// we are within the deferred time so skip the test
return xit(description, body);
}
return it(description, body);
};
export const itFailsWindows = (
description: string,
body: Mocha.Func
): Mocha.Test => {
if (os.platform() === 'win32') {
return xit(description, body);
}
return it(description, body);
};
export const describeFailsFirefox = (
description: string,
body: (this: Mocha.Suite) => void
): void | Mocha.Suite => {
if (isFirefox) {
return xdescribe(description, body);
} else {
return describe(description, body);
}
};
export const describeChromeOnly = (
description: string,
body: (this: Mocha.Suite) => void
): Mocha.Suite | void => {
if (isChrome) {
return describe(description, body);
}
};
if (process.env['MOCHA_WORKER_ID'] === '0') {
console.log( console.log(
`Running unit tests with: `Running unit tests with:
-> product: ${product} -> product: ${product}
@ -290,7 +198,7 @@ export const setupTestBrowserHooks = (): void => {
}); });
after(async () => { after(async () => {
await state.browser!.close(); await state.browser?.close();
state.browser = undefined; state.browser = undefined;
}); });
}; };
@ -302,7 +210,7 @@ export const setupTestPageAndContextHooks = (): void => {
}); });
afterEach(async () => { afterEach(async () => {
await state.context!.close(); await state.context?.close();
state.context = undefined; state.context = undefined;
state.page = undefined; state.page = undefined;
}); });
@ -387,3 +295,14 @@ export const shortWaitForArrayToHaveAtLeastNElements = async (
}); });
} }
}; };
export const createTimeout = <T>(
n: number,
value?: T
): Promise<T | undefined> => {
return new Promise(resolve => {
setTimeout(() => {
return resolve(value);
}, n);
});
};

View file

@ -137,24 +137,21 @@ describe('Mouse', function () {
}) })
).toBe('button-91'); ).toBe('button-91');
}); });
it( it('should trigger hover state with removed window.Node', async () => {
'should trigger hover state with removed window.Node', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/scrollable.html'); await page.goto(server.PREFIX + '/input/scrollable.html');
await page.evaluate(() => {
// @ts-expect-error Expected.
return delete window.Node;
});
await page.hover('#button-6');
expect(
await page.evaluate(() => { await page.evaluate(() => {
// @ts-expect-error Expected. return document.querySelector('button:hover')!.id;
return delete window.Node; })
}); ).toBe('button-6');
await page.hover('#button-6'); });
expect(
await page.evaluate(() => {
return document.querySelector('button:hover')!.id;
})
).toBe('button-6');
}
);
it('should set modifier keys on click', async () => { it('should set modifier keys on click', async () => {
const {page, server, isFirefox} = getTestState(); const {page, server, isFirefox} = getTestState();

View file

@ -122,28 +122,22 @@ describe('navigation', function () {
const response = await page.goto(server.PREFIX + '/grid.html'); const response = await page.goto(server.PREFIX + '/grid.html');
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
}); });
it( it('should navigate to empty page with networkidle0', async () => {
'should navigate to empty page with networkidle0', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, { const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle0', waitUntil: 'networkidle0',
}); });
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
} });
); it('should navigate to empty page with networkidle2', async () => {
it( const {page, server} = getTestState();
'should navigate to empty page with networkidle2',
async () => {
const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, { const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle2', waitUntil: 'networkidle2',
}); });
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
} });
);
it('should fail when navigating to bad url', async () => { it('should fail when navigating to bad url', async () => {
const {page, isChrome} = getTestState(); const {page, isChrome} = getTestState();
@ -332,85 +326,79 @@ describe('navigation', function () {
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
expect(response.url()).toBe(server.EMPTY_PAGE); expect(response.url()).toBe(server.EMPTY_PAGE);
}); });
it( it('should wait for network idle to succeed navigation', async () => {
'should wait for network idle to succeed navigation', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
let responses: ServerResponse[] = []; let responses: ServerResponse[] = [];
// Hold on to a bunch of requests without answering. // Hold on to a bunch of requests without answering.
server.setRoute('/fetch-request-a.js', (_req, res) => { server.setRoute('/fetch-request-a.js', (_req, res) => {
return responses.push(res); return responses.push(res);
}); });
server.setRoute('/fetch-request-b.js', (_req, res) => { server.setRoute('/fetch-request-b.js', (_req, res) => {
return responses.push(res); return responses.push(res);
}); });
server.setRoute('/fetch-request-c.js', (_req, res) => { server.setRoute('/fetch-request-c.js', (_req, res) => {
return responses.push(res); return responses.push(res);
}); });
server.setRoute('/fetch-request-d.js', (_req, res) => { server.setRoute('/fetch-request-d.js', (_req, res) => {
return responses.push(res); return responses.push(res);
}); });
const initialFetchResourcesRequested = Promise.all([ const initialFetchResourcesRequested = Promise.all([
server.waitForRequest('/fetch-request-a.js'), server.waitForRequest('/fetch-request-a.js'),
server.waitForRequest('/fetch-request-b.js'), server.waitForRequest('/fetch-request-b.js'),
server.waitForRequest('/fetch-request-c.js'), server.waitForRequest('/fetch-request-c.js'),
]); ]);
const secondFetchResourceRequested = server.waitForRequest( const secondFetchResourceRequested = server.waitForRequest(
'/fetch-request-d.js' '/fetch-request-d.js'
); );
// Navigate to a page which loads immediately and then does a bunch of // Navigate to a page which loads immediately and then does a bunch of
// requests via javascript's fetch method. // requests via javascript's fetch method.
const navigationPromise = page.goto( const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', {
server.PREFIX + '/networkidle.html', waitUntil: 'networkidle0',
{ });
waitUntil: 'networkidle0', // Track when the navigation gets completed.
} let navigationFinished = false;
); navigationPromise.then(() => {
// Track when the navigation gets completed. return (navigationFinished = true);
let navigationFinished = false; });
navigationPromise.then(() => {
return (navigationFinished = true);
});
// Wait for the page's 'load' event. // Wait for the page's 'load' event.
await new Promise(fulfill => { await new Promise(fulfill => {
return page.once('load', fulfill); return page.once('load', fulfill);
}); });
expect(navigationFinished).toBe(false); expect(navigationFinished).toBe(false);
// Wait for the initial three resources to be requested. // Wait for the initial three resources to be requested.
await initialFetchResourcesRequested; await initialFetchResourcesRequested;
// Expect navigation still to be not finished. // Expect navigation still to be not finished.
expect(navigationFinished).toBe(false); expect(navigationFinished).toBe(false);
// Respond to initial requests. // Respond to initial requests.
for (const response of responses) { for (const response of responses) {
response.statusCode = 404; response.statusCode = 404;
response.end(`File not found`); response.end(`File not found`);
}
// Reset responses array
responses = [];
// Wait for the second round to be requested.
await secondFetchResourceRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Respond to requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
}
const response = (await navigationPromise)!;
// Expect navigation to succeed.
expect(response.ok()).toBe(true);
} }
);
// Reset responses array
responses = [];
// Wait for the second round to be requested.
await secondFetchResourceRequested;
// Expect navigation still to be not finished.
expect(navigationFinished).toBe(false);
// Respond to requests.
for (const response of responses) {
response.statusCode = 404;
response.end(`File not found`);
}
const response = (await navigationPromise)!;
// Expect navigation to succeed.
expect(response.ok()).toBe(true);
});
it('should not leak listeners during navigation', async () => { it('should not leak listeners during navigation', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -459,38 +447,32 @@ describe('navigation', function () {
process.removeListener('warning', warningHandler); process.removeListener('warning', warningHandler);
expect(warning).toBe(null); expect(warning).toBe(null);
}); });
it( it('should navigate to dataURL and fire dataURL requests', async () => {
'should navigate to dataURL and fire dataURL requests', const {page} = getTestState();
async () => {
const {page} = getTestState();
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
page.on('request', request => { page.on('request', request => {
return !utils.isFavicon(request) && requests.push(request); return !utils.isFavicon(request) && requests.push(request);
}); });
const dataURL = 'data:text/html,<div>yo</div>'; const dataURL = 'data:text/html,<div>yo</div>';
const response = (await page.goto(dataURL))!; const response = (await page.goto(dataURL))!;
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(requests.length).toBe(1); expect(requests.length).toBe(1);
expect(requests[0]!.url()).toBe(dataURL); expect(requests[0]!.url()).toBe(dataURL);
} });
); it('should navigate to URL with hash and fire requests without hash', async () => {
it( const {page, server} = getTestState();
'should navigate to URL with hash and fire requests without hash',
async () => {
const {page, server} = getTestState();
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
page.on('request', request => { page.on('request', request => {
return !utils.isFavicon(request) && requests.push(request); return !utils.isFavicon(request) && requests.push(request);
}); });
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!; const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(response.url()).toBe(server.EMPTY_PAGE); expect(response.url()).toBe(server.EMPTY_PAGE);
expect(requests.length).toBe(1); expect(requests.length).toBe(1);
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
} });
);
it('should work with self requesting page', async () => { it('should work with self requesting page', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -614,13 +596,11 @@ describe('navigation', function () {
expect(response).toBe(null); expect(response).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/replaced.html'); expect(page.url()).toBe(server.PREFIX + '/replaced.html');
}); });
it( it('should work with DOM history.back()/history.forward()', async () => {
'should work with DOM history.back()/history.forward()', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent(` await page.setContent(`
<a id=back onclick='javascript:goBack()'>back</a> <a id=back onclick='javascript:goBack()'>back</a>
<a id=forward onclick='javascript:goForward()'>forward</a> <a id=forward onclick='javascript:goForward()'>forward</a>
<script> <script>
@ -630,46 +610,42 @@ describe('navigation', function () {
history.pushState({}, '', '/second.html'); history.pushState({}, '', '/second.html');
</script> </script>
`); `);
expect(page.url()).toBe(server.PREFIX + '/second.html'); expect(page.url()).toBe(server.PREFIX + '/second.html');
const [backResponse] = await Promise.all([ const [backResponse] = await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.click('a#back'), page.click('a#back'),
]); ]);
expect(backResponse).toBe(null); expect(backResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/first.html'); expect(page.url()).toBe(server.PREFIX + '/first.html');
const [forwardResponse] = await Promise.all([ const [forwardResponse] = await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.click('a#forward'), page.click('a#forward'),
]); ]);
expect(forwardResponse).toBe(null); expect(forwardResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/second.html'); expect(page.url()).toBe(server.PREFIX + '/second.html');
} });
); it('should work when subframe issues window.stop()', async () => {
it( const {page, server} = getTestState();
'should work when subframe issues window.stop()',
async () => {
const {page, server} = getTestState();
server.setRoute('/frames/style.css', () => {}); server.setRoute('/frames/style.css', () => {});
const navigationPromise = page.goto( const navigationPromise = page.goto(
server.PREFIX + '/frames/one-frame.html' server.PREFIX + '/frames/one-frame.html'
); );
const frame = await utils.waitEvent(page, 'frameattached'); const frame = await utils.waitEvent(page, 'frameattached');
await new Promise<void>(fulfill => { await new Promise<void>(fulfill => {
page.on('framenavigated', f => { page.on('framenavigated', f => {
if (f === frame) { if (f === frame) {
fulfill(); fulfill();
} }
});
}); });
await Promise.all([ });
frame.evaluate(() => { await Promise.all([
return window.stop(); frame.evaluate(() => {
}), return window.stop();
navigationPromise, }),
]); navigationPromise,
} ]);
); });
}); });
describe('Page.goBack', function () { describe('Page.goBack', function () {

View file

@ -22,9 +22,6 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
itFailsFirefox,
itChromeOnly,
itFirefoxOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js'; import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
@ -114,13 +111,13 @@ describe('network', function () {
}); });
describe('Request.headers', function () { describe('Request.headers', function () {
itChromeOnly('should define Chrome as user agent header', async () => { it('should define Chrome as user agent header', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const response = (await page.goto(server.EMPTY_PAGE))!; const response = (await page.goto(server.EMPTY_PAGE))!;
expect(response.request().headers()['user-agent']).toContain('Chrome'); expect(response.request().headers()['user-agent']).toContain('Chrome');
}); });
itFirefoxOnly('should define Firefox as user agent header', async () => { it('should define Firefox as user agent header', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const response = (await page.goto(server.EMPTY_PAGE))!; const response = (await page.goto(server.EMPTY_PAGE))!;
@ -655,10 +652,7 @@ describe('network', function () {
expect(requests.get('script.js').isNavigationRequest()).toBe(false); expect(requests.get('script.js').isNavigationRequest()).toBe(false);
expect(requests.get('style.css').isNavigationRequest()).toBe(false); expect(requests.get('style.css').isNavigationRequest()).toBe(false);
}); });
// This `itFailsFirefox` should be preserved in mozilla-central (Firefox). it('should work when navigating to image', async () => {
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748254
// or https://github.com/puppeteer/puppeteer/pull/7846
itFailsFirefox('should work when navigating to image', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];

View file

@ -16,17 +16,11 @@
import utils from './utils.js'; import utils from './utils.js';
import expect from 'expect'; import expect from 'expect';
import { import {getTestState} from './mocha-utils.js';
getTestState, import {Browser, BrowserContext} from '../../lib/cjs/puppeteer/api/Browser.js';
describeChromeOnly,
} from './mocha-utils.js';
import {
Browser,
BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
describeChromeOnly('OOPIF', function () { describe('OOPIF', function () {
/* We use a special browser for this test as we need the --site-per-process flag */ /* We use a special browser for this test as we need the --site-per-process flag */
let browser: Browser; let browser: Browser;
let context: BrowserContext; let context: BrowserContext;
@ -206,6 +200,7 @@ describeChromeOnly('OOPIF', function () {
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE); await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
expect(frame.url()).toBe(server.EMPTY_PAGE); expect(frame.url()).toBe(server.EMPTY_PAGE);
}); });
it('should support evaluating in oop iframes', async () => { it('should support evaluating in oop iframes', async () => {
const {server} = getTestState(); const {server} = getTestState();

View file

@ -23,7 +23,6 @@ import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js';
import { import {
getTestState, getTestState,
itFailsFirefox,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
@ -546,39 +545,69 @@ describe('Page', function () {
it('should work', async () => { it('should work', async () => {
const {page} = getTestState(); const {page} = getTestState();
// Instantiate an object // Create a custom class
await page.evaluate(() => { const classHandle = await page.evaluateHandle(() => {
return ((globalThis as any).set = new Set(['hello', 'world'])); return class CustomClass {};
}); });
const prototypeHandle = await page.evaluateHandle(() => {
return Set.prototype;
});
const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate(objects => {
return objects.length;
}, objectsHandle);
expect(count).toBe(1);
const values = await page.evaluate(objects => {
return Array.from(objects[0]!.values());
}, objectsHandle);
expect(values).toEqual(['hello', 'world']);
});
it('should work for non-blank page', async () => {
const {page, server} = getTestState();
// Instantiate an object // Create an instance.
await page.goto(server.EMPTY_PAGE); await page.evaluate(CustomClass => {
await page.evaluate(() => { // @ts-expect-error: Different context.
return ((globalThis as any).set = new Set(['hello', 'world'])); self.customClass = new CustomClass();
}); }, classHandle);
const prototypeHandle = await page.evaluateHandle(() => {
return Set.prototype; // Validate only one has been added.
}); const prototypeHandle = await page.evaluateHandle(CustomClass => {
return CustomClass.prototype;
}, classHandle);
const objectsHandle = await page.queryObjects(prototypeHandle); const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate(objects => { await expect(
return objects.length; page.evaluate(objects => {
}, objectsHandle); return objects.length;
expect(count).toBe(1); }, objectsHandle)
).resolves.toBe(1);
// Check that instances.
await expect(
page.evaluate(objects => {
// @ts-expect-error: Different context.
return objects[0] === self.customClass;
}, objectsHandle)
).resolves.toBeTruthy();
});
it('should work for non-trivial page', async () => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE);
// Create a custom class
const classHandle = await page.evaluateHandle(() => {
return class CustomClass {};
});
// Create an instance.
await page.evaluate(CustomClass => {
// @ts-expect-error: Different context.
self.customClass = new CustomClass();
}, classHandle);
// Validate only one has been added.
const prototypeHandle = await page.evaluateHandle(CustomClass => {
return CustomClass.prototype;
}, classHandle);
const objectsHandle = await page.queryObjects(prototypeHandle);
await expect(
page.evaluate(objects => {
return objects.length;
}, objectsHandle)
).resolves.toBe(1);
// Check that instances.
await expect(
page.evaluate(objects => {
// @ts-expect-error: Different context.
return objects[0] === self.customClass;
}, objectsHandle)
).resolves.toBeTruthy();
}); });
it('should fail for disposed handles', async () => { it('should fail for disposed handles', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -1651,7 +1680,7 @@ describe('Page', function () {
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'}); await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return (globalThis as any).__es6injected; return (window as unknown as {__es6injected: number}).__es6injected;
}) })
).toBe(42); ).toBe(42);
}); });
@ -1664,10 +1693,12 @@ describe('Page', function () {
path: path.join(__dirname, '../assets/es6/es6pathimport.js'), path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
type: 'module', type: 'module',
}); });
await page.waitForFunction('window.__es6injected'); await page.waitForFunction(() => {
return (window as unknown as {__es6injected: number}).__es6injected;
});
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return (globalThis as any).__es6injected; return (window as unknown as {__es6injected: number}).__es6injected;
}) })
).toBe(42); ).toBe(42);
}); });
@ -1680,10 +1711,12 @@ describe('Page', function () {
content: `import num from '/es6/es6module.js';window.__es6injected = num;`, content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
type: 'module', type: 'module',
}); });
await page.waitForFunction('window.__es6injected'); await page.waitForFunction(() => {
return (window as unknown as {__es6injected: number}).__es6injected;
});
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return (globalThis as any).__es6injected; return (window as unknown as {__es6injected: number}).__es6injected;
}) })
).toBe(42); ).toBe(42);
}); });
@ -1758,7 +1791,7 @@ describe('Page', function () {
}); });
// @see https://github.com/puppeteer/puppeteer/issues/4840 // @see https://github.com/puppeteer/puppeteer/issues/4840
xit('should throw when added with content to the CSP page', async () => { it.skip('should throw when added with content to the CSP page', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
@ -1854,7 +1887,7 @@ describe('Page', function () {
path: path.join(__dirname, '../assets/injectedstyle.css'), path: path.join(__dirname, '../assets/injectedstyle.css'),
}); });
const styleHandle = (await page.$('style'))!; const styleHandle = (await page.$('style'))!;
const styleContent = await page.evaluate(style => { const styleContent = await page.evaluate((style: HTMLStyleElement) => {
return style.innerHTML; return style.innerHTML;
}, styleHandle); }, styleHandle);
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
@ -2002,10 +2035,7 @@ describe('Page', function () {
expect(size).toBeGreaterThan(0); expect(size).toBeGreaterThan(0);
}); });
// This test should be skipped in mozilla-central (Firefox). it('should respect timeout', async () => {
// It intermittently makes the whole test suite fail.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255
itFailsFirefox('should respect timeout', async () => {
const {isHeadless, page, server, puppeteer} = getTestState(); const {isHeadless, page, server, puppeteer} = getTestState();
if (!isHeadless) { if (!isHeadless) {
return; return;
@ -2236,7 +2266,7 @@ describe('Page', function () {
}); });
describe('Page.Events.Close', function () { describe('Page.Events.Close', function () {
itFailsFirefox('should work with window.close', async () => { it('should work with window.close', async () => {
const {page, context} = getTestState(); const {page, context} = getTestState();
const newPagePromise = new Promise<Page>(fulfill => { const newPagePromise = new Promise<Page>(fulfill => {

View file

@ -17,13 +17,9 @@
import expect from 'expect'; import expect from 'expect';
import http from 'http'; import http from 'http';
import os from 'os'; import os from 'os';
import { import {getTestState} from './mocha-utils.js';
getTestState,
describeFailsFirefox,
itFailsWindows,
} from './mocha-utils.js';
import type {Server, IncomingMessage, ServerResponse} from 'http'; import type {Server, IncomingMessage, ServerResponse} from 'http';
import type {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; import type {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
import type {AddressInfo} from 'net'; import type {AddressInfo} from 'net';
import {TestServer} from '../../utils/testserver/lib/index.js'; import {TestServer} from '../../utils/testserver/lib/index.js';
@ -53,7 +49,7 @@ function getEmptyPageUrl(server: TestServer): string {
return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`; return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
} }
describeFailsFirefox('request proxy', () => { describe('request proxy', () => {
let browser: Browser; let browser: Browser;
let proxiedRequestUrls: string[]; let proxiedRequestUrls: string[];
let proxyServer: Server; let proxyServer: Server;
@ -194,28 +190,25 @@ describeFailsFirefox('request proxy', () => {
/** /**
* See issues #7873, #7719, and #7698. * See issues #7873, #7719, and #7698.
*/ */
itFailsWindows( it('should proxy requests when configured at context level', async () => {
'should proxy requests when configured at context level', const {puppeteer, defaultBrowserOptions, server} = getTestState();
async () => { const emptyPageUrl = getEmptyPageUrl(server);
const {puppeteer, defaultBrowserOptions, server} = getTestState();
const emptyPageUrl = getEmptyPageUrl(server);
browser = await puppeteer.launch({ browser = await puppeteer.launch({
...defaultBrowserOptions, ...defaultBrowserOptions,
args: defaultArgs, args: defaultArgs,
}); });
const context = await browser.createIncognitoBrowserContext({ const context = await browser.createIncognitoBrowserContext({
proxyServer: proxyServerUrl, proxyServer: proxyServerUrl,
}); });
const page = await context.newPage(); const page = await context.newPage();
const response = (await page.goto(emptyPageUrl))!; const response = (await page.goto(emptyPageUrl))!;
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
expect(proxiedRequestUrls).toEqual([emptyPageUrl]); expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
} });
);
it('should respect proxy bypass list when configured at context level', async () => { it('should respect proxy bypass list when configured at context level', async () => {
const {puppeteer, defaultBrowserOptions, server} = getTestState(); const {puppeteer, defaultBrowserOptions, server} = getTestState();

View file

@ -94,6 +94,200 @@ describe('Query handler tests', function () {
}); });
}); });
describe('Text selectors', function () {
describe('in Page', function () {
it('should query existing element', async () => {
const {page} = getTestState();
await page.setContent('<section>test</section>');
expect(await page.$('text/test')).toBeTruthy();
expect((await page.$$('text/test')).length).toBe(1);
});
it('should return empty array for non-existing element', async () => {
const {page} = getTestState();
expect(await page.$('text/test')).toBeFalsy();
expect((await page.$$('text/test')).length).toBe(0);
});
it('should return first element', async () => {
const {page} = getTestState();
await page.setContent('<div id="1">a</div><div>a</div>');
const element = await page.$('text/a');
expect(
await element?.evaluate(e => {
return e.id;
})
).toBe('1');
});
it('should return multiple elements', async () => {
const {page} = getTestState();
await page.setContent('<div>a</div><div>a</div>');
const elements = await page.$$('text/a');
expect(elements.length).toBe(2);
});
it('should pierce shadow DOM', async () => {
const {page} = getTestState();
await page.evaluate(() => {
const div = document.createElement('div');
const shadow = div.attachShadow({mode: 'open'});
const diva = document.createElement('div');
shadow.append(diva);
const divb = document.createElement('div');
shadow.append(divb);
diva.innerHTML = 'a';
divb.innerHTML = 'b';
document.body.append(div);
});
const element = await page.$('text/a');
expect(
await element?.evaluate(e => {
return e.textContent;
})
).toBe('a');
});
it('should query deeply nested text', async () => {
const {page} = getTestState();
await page.setContent('<div><div>a</div><div>b</div></div>');
const element = await page.$('text/a');
expect(
await element?.evaluate(e => {
return e.textContent;
})
).toBe('a');
});
it('should query inputs', async () => {
const {page} = getTestState();
await page.setContent('<input value="a">');
const element = (await page.$(
'text/a'
)) as ElementHandle<HTMLInputElement>;
expect(
await element?.evaluate(e => {
return e.value;
})
).toBe('a');
});
it('should not query radio', async () => {
const {page} = getTestState();
await page.setContent('<radio value="a">');
expect(await page.$('text/a')).toBeNull();
});
it('should query text spanning multiple elements', async () => {
const {page} = getTestState();
await page.setContent('<div><span>a</span> <span>b</span><div>');
const element = await page.$('text/a b');
expect(
await element?.evaluate(e => {
return e.textContent;
})
).toBe('a b');
});
it('should clear caches', async () => {
const {page} = getTestState();
await page.setContent(
'<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>'
);
const div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>;
const input = (await page.$(
'#target2'
)) as ElementHandle<HTMLInputElement>;
await div.evaluate(div => {
div.textContent = 'text';
});
expect(
await page.$eval(`text/text`, e => {
return e.id;
})
).toBe('target1');
await div.evaluate(div => {
div.textContent = 'foo';
});
expect(
await page.$eval(`text/text`, e => {
return e.id;
})
).toBe('target2');
await input.evaluate(input => {
input.value = '';
});
await input.type('foo');
expect(
await page.$eval(`text/text`, e => {
return e.id;
})
).toBe('target3');
await div.evaluate(div => {
div.textContent = 'text';
});
await input.evaluate(input => {
input.value = '';
});
await input.type('text');
expect(
await page.$$eval(`text/text`, es => {
return es.length;
})
).toBe(3);
await div.evaluate(div => {
div.textContent = 'foo';
});
expect(
await page.$$eval(`text/text`, es => {
return es.length;
})
).toBe(2);
await input.evaluate(input => {
input.value = '';
});
await input.type('foo');
expect(
await page.$$eval(`text/text`, es => {
return es.length;
})
).toBe(1);
});
});
describe('in ElementHandles', function () {
it('should query existing element', async () => {
const {page} = getTestState();
await page.setContent('<div class="a"><span>a</span></div>');
const elementHandle = (await page.$('div'))!;
expect(await elementHandle.$(`text/a`)).toBeTruthy();
expect((await elementHandle.$$(`text/a`)).length).toBe(1);
});
it('should return null for non-existing element', async () => {
const {page} = getTestState();
await page.setContent('<div class="a"></div>');
const elementHandle = (await page.$('div'))!;
expect(await elementHandle.$(`text/a`)).toBeFalsy();
expect((await elementHandle.$$(`text/a`)).length).toBe(0);
});
});
});
describe('XPath selectors', function () { describe('XPath selectors', function () {
describe('in Page', function () { describe('in Page', function () {
it('should query existing element', async () => { it('should query existing element', async () => {

View file

@ -19,8 +19,6 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
itHeadfulOnly,
itChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
describe('Screenshots', function () { describe('Screenshots', function () {
@ -67,23 +65,20 @@ describe('Screenshots', function () {
}); });
expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png'); expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
}); });
it( it('should get screenshot bigger than the viewport', async () => {
'should get screenshot bigger than the viewport', const {page, server} = getTestState();
async () => { await page.setViewport({width: 50, height: 50});
const {page, server} = getTestState(); await page.goto(server.PREFIX + '/grid.html');
await page.setViewport({width: 50, height: 50}); const screenshot = await page.screenshot({
await page.goto(server.PREFIX + '/grid.html'); clip: {
const screenshot = await page.screenshot({ x: 25,
clip: { y: 25,
x: 25, width: 100,
y: 25, height: 100,
width: 100, },
height: 100, });
}, expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}); });
expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
}
);
it('should run in parallel', async () => { it('should run in parallel', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -205,7 +200,7 @@ describe('Screenshots', function () {
'screenshot-sanity.png' 'screenshot-sanity.png'
); );
}); });
itHeadfulOnly('should work in "fromSurface: false" mode', async () => { it('should work in "fromSurface: false" mode', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.setViewport({width: 500, height: 500}); await page.setViewport({width: 500, height: 500});
@ -230,7 +225,7 @@ describe('Screenshots', function () {
const screenshot = await elementHandle.screenshot(); const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-bounding-box.png'); expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
}); });
itChromeOnly('should work with a null viewport', async () => { it('should work with a null viewport', async () => {
const {defaultBrowserOptions, puppeteer, server} = getTestState(); const {defaultBrowserOptions, puppeteer, server} = getTestState();
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
@ -270,14 +265,12 @@ describe('Screenshots', function () {
const screenshot = await elementHandle.screenshot(); const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
}); });
it( it('should capture full element when larger than viewport', async () => {
'should capture full element when larger than viewport', const {page} = getTestState();
async () => {
const {page} = getTestState();
await page.setViewport({width: 500, height: 500}); await page.setViewport({width: 500, height: 500});
await page.setContent(` await page.setContent(`
something above something above
<style> <style>
div.to-screenshot { div.to-screenshot {
@ -292,22 +285,21 @@ describe('Screenshots', function () {
</style> </style>
<div class="to-screenshot"></div> <div class="to-screenshot"></div>
`); `);
const elementHandle = (await page.$('div.to-screenshot'))!; const elementHandle = (await page.$('div.to-screenshot'))!;
const screenshot = await elementHandle.screenshot(); const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden( expect(screenshot).toBeGolden(
'screenshot-element-larger-than-viewport.png' 'screenshot-element-larger-than-viewport.png'
); );
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return { return {
w: window.innerWidth, w: window.innerWidth,
h: window.innerHeight, h: window.innerHeight,
}; };
}) })
).toEqual({w: 500, h: 500}); ).toEqual({w: 500, h: 500});
} });
);
it('should scroll element into view', async () => { it('should scroll element into view', async () => {
const {page} = getTestState(); const {page} = getTestState();

View file

@ -20,11 +20,11 @@ import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {Target} from '../../lib/cjs/puppeteer/common/Target.js'; import {Target} from '../../lib/cjs/puppeteer/common/Target.js';
import { import {
getTestState, getTestState,
itFailsFirefox,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
import utils from './utils.js'; import utils from './utils.js';
const {waitEvent} = utils; const {waitEvent} = utils;
describe('Target', function () { describe('Target', function () {
@ -79,10 +79,7 @@ describe('Target', function () {
).toBe('Hello world'); ).toBe('Hello world');
expect(await originalPage.$('body')).toBeTruthy(); expect(await originalPage.$('body')).toBeTruthy();
}); });
// This test should be skipped in mozilla-central (Firefox). it('should be able to use async waitForTarget', async () => {
// It intermittently makes some tests fail and triggers errors in the test hooks.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255
itFailsFirefox('should be able to use async waitForTarget', async () => {
const {page, server, context} = getTestState(); const {page, server, context} = getTestState();
const [otherPage] = await Promise.all([ const [otherPage] = await Promise.all([
@ -104,88 +101,82 @@ describe('Target', function () {
); );
expect(page).not.toEqual(otherPage); expect(page).not.toEqual(otherPage);
}); });
it( it('should report when a new page is created and closed', async () => {
'should report when a new page is created and closed', const {page, server, context} = getTestState();
async () => {
const {page, server, context} = getTestState();
const [otherPage] = await Promise.all([ const [otherPage] = await Promise.all([
context context
.waitForTarget(target => { .waitForTarget(target => {
return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
})
.then(target => {
return target.page();
}),
page.evaluate((url: string) => {
return window.open(url);
}, server.CROSS_PROCESS_PREFIX + '/empty.html'),
]);
expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
expect(
await otherPage!.evaluate(() => {
return ['Hello', 'world'].join(' ');
}) })
).toBe('Hello world'); .then(target => {
expect(await otherPage!.$('body')).toBeTruthy();
let allPages = await context.pages();
expect(allPages).toContain(page);
expect(allPages).toContain(otherPage);
const closePagePromise = new Promise(fulfill => {
return context.once('targetdestroyed', target => {
return fulfill(target.page());
});
});
await otherPage!.close();
expect(await closePagePromise).toBe(otherPage);
allPages = (await Promise.all(
context.targets().map(target => {
return target.page(); return target.page();
}) }),
)) as Page[]; page.evaluate((url: string) => {
expect(allPages).toContain(page); return window.open(url);
expect(allPages).not.toContain(otherPage); }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
} ]);
); expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
it( expect(
'should report when a service worker is created and destroyed', await otherPage!.evaluate(() => {
async () => { return ['Hello', 'world'].join(' ');
const {page, server, context} = getTestState(); })
).toBe('Hello world');
expect(await otherPage!.$('body')).toBeTruthy();
await page.goto(server.EMPTY_PAGE); let allPages = await context.pages();
const createdTarget = new Promise<Target>(fulfill => { expect(allPages).toContain(page);
return context.once('targetcreated', target => { expect(allPages).toContain(otherPage);
return fulfill(target);
}); const closePagePromise = new Promise(fulfill => {
return context.once('targetdestroyed', target => {
return fulfill(target.page());
}); });
});
await otherPage!.close();
expect(await closePagePromise).toBe(otherPage);
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); allPages = (await Promise.all(
context.targets().map(target => {
return target.page();
})
)) as Page[];
expect(allPages).toContain(page);
expect(allPages).not.toContain(otherPage);
});
it('should report when a service worker is created and destroyed', async () => {
const {page, server, context} = getTestState();
expect((await createdTarget).type()).toBe('service_worker'); await page.goto(server.EMPTY_PAGE);
expect((await createdTarget).url()).toBe( const createdTarget = new Promise<Target>(fulfill => {
server.PREFIX + '/serviceworkers/empty/sw.js' return context.once('targetcreated', target => {
); return fulfill(target);
const destroyedTarget = new Promise(fulfill => {
return context.once('targetdestroyed', target => {
return fulfill(target);
});
}); });
await page.evaluate(() => { });
return (
globalThis as unknown as { await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
registrationPromise: Promise<{unregister: () => void}>;
} expect((await createdTarget).type()).toBe('service_worker');
).registrationPromise.then((registration: any) => { expect((await createdTarget).url()).toBe(
return registration.unregister(); server.PREFIX + '/serviceworkers/empty/sw.js'
}); );
const destroyedTarget = new Promise(fulfill => {
return context.once('targetdestroyed', target => {
return fulfill(target);
}); });
expect(await destroyedTarget).toBe(await createdTarget); });
} await page.evaluate(() => {
); return (
globalThis as unknown as {
registrationPromise: Promise<{unregister: () => void}>;
}
).registrationPromise.then((registration: any) => {
return registration.unregister();
});
});
expect(await destroyedTarget).toBe(await createdTarget);
});
it('should create a worker from a service worker', async () => { it('should create a worker from a service worker', async () => {
const {page, server, context} = getTestState(); const {page, server, context} = getTestState();
@ -271,36 +262,33 @@ describe('Target', function () {
expect(targetChanged).toBe(false); expect(targetChanged).toBe(false);
context.removeListener('targetchanged', listener); context.removeListener('targetchanged', listener);
}); });
it( it('should not crash while redirecting if original request was missed', async () => {
'should not crash while redirecting if original request was missed', const {page, server, context} = getTestState();
async () => {
const {page, server, context} = getTestState();
let serverResponse!: ServerResponse; let serverResponse!: ServerResponse;
server.setRoute('/one-style.css', (_req, res) => { server.setRoute('/one-style.css', (_req, res) => {
return (serverResponse = res); return (serverResponse = res);
}); });
// Open a new page. Use window.open to connect to the page later. // Open a new page. Use window.open to connect to the page later.
await Promise.all([ await Promise.all([
page.evaluate((url: string) => { page.evaluate((url: string) => {
return window.open(url); return window.open(url);
}, server.PREFIX + '/one-style.html'), }, server.PREFIX + '/one-style.html'),
server.waitForRequest('/one-style.css'), server.waitForRequest('/one-style.css'),
]); ]);
// Connect to the opened page. // Connect to the opened page.
const target = await context.waitForTarget(target => { const target = await context.waitForTarget(target => {
return target.url().includes('one-style.html'); return target.url().includes('one-style.html');
}); });
const newPage = (await target.page())!; const newPage = (await target.page())!;
// Issue a redirect. // Issue a redirect.
serverResponse.writeHead(302, {location: '/injectedstyle.css'}); serverResponse.writeHead(302, {location: '/injectedstyle.css'});
serverResponse.end(); serverResponse.end();
// Wait for the new page to load. // Wait for the new page to load.
await waitEvent(newPage, 'load'); await waitEvent(newPage, 'load');
// Cleanup. // Cleanup.
await newPage.close(); await newPage.close();
} });
);
it('should have an opener', async () => { it('should have an opener', async () => {
const {page, server, context} = getTestState(); const {page, server, context} = getTestState();

View file

@ -17,11 +17,11 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import expect from 'expect'; import expect from 'expect';
import {getTestState, describeChromeOnly} from './mocha-utils.js'; import {getTestState} from './mocha-utils.js';
import {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; import {Browser} from '../../lib/cjs/puppeteer/api/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
describeChromeOnly('Tracing', function () { describe('Tracing', function () {
let outputFile!: string; let outputFile!: string;
let browser!: Browser; let browser!: Browser;
let page!: Page; let page!: Page;

View file

@ -17,6 +17,7 @@
import expect from 'expect'; import expect from 'expect';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
createTimeout,
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
@ -31,9 +32,9 @@ describe('waittask specs', function () {
it('should accept a string', async () => { it('should accept a string', async () => {
const {page} = getTestState(); const {page} = getTestState();
const watchdog = page.waitForFunction('window.__FOO === 1'); const watchdog = page.waitForFunction('self.__FOO === 1');
await page.evaluate(() => { await page.evaluate(() => {
return ((globalThis as any).__FOO = 1); return ((self as unknown as {__FOO: number}).__FOO = 1);
}); });
await watchdog; await watchdog;
}); });
@ -46,61 +47,25 @@ describe('waittask specs', function () {
await page.waitForFunction(() => { await page.waitForFunction(() => {
if (!(globalThis as any).__RELOADED) { if (!(globalThis as any).__RELOADED) {
window.location.reload(); window.location.reload();
return false;
} }
return true; return true;
}); });
}); });
it('should poll on interval', async () => { it('should poll on interval', async () => {
const {page} = getTestState(); const {page} = getTestState();
let success = false;
const startTime = Date.now(); const startTime = Date.now();
const polling = 100; const polling = 100;
const watchdog = page const watchdog = page.waitForFunction(
.waitForFunction( () => {
() => { return (globalThis as any).__FOO === 'hit';
return (globalThis as any).__FOO === 'hit'; },
}, {polling}
{ );
polling,
}
)
.then(() => {
return (success = true);
});
await page.evaluate(() => { await page.evaluate(() => {
return ((globalThis as any).__FOO = 'hit'); setTimeout(() => {
}); (globalThis as any).__FOO = 'hit';
expect(success).toBe(false); }, 50);
await page.evaluate(() => {
return document.body.appendChild(document.createElement('div'));
});
await watchdog;
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
});
it('should poll on interval async', async () => {
const {page} = getTestState();
let success = false;
const startTime = Date.now();
const polling = 100;
const watchdog = page
.waitForFunction(
async () => {
return (globalThis as any).__FOO === 'hit';
},
{
polling,
}
)
.then(() => {
return (success = true);
});
await page.evaluate(async () => {
return ((globalThis as any).__FOO = 'hit');
});
expect(success).toBe(false);
await page.evaluate(async () => {
return document.body.appendChild(document.createElement('div'));
}); });
await watchdog; await watchdog;
expect(Date.now() - startTime).not.toBeLessThan(polling / 2); expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
@ -212,26 +177,6 @@ describe('waittask specs', function () {
]); ]);
expect(error).toBeUndefined(); expect(error).toBeUndefined();
}); });
it('should throw on bad polling value', async () => {
const {page} = getTestState();
let error!: Error;
try {
await page.waitForFunction(
() => {
return !!document.body;
},
{
polling: 'unknown',
}
);
} catch (error_) {
if (isErrorLike(error_)) {
error = error_ as Error;
}
}
expect(error?.message).toContain('polling');
});
it('should throw negative polling interval', async () => { it('should throw negative polling interval', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -299,23 +244,34 @@ describe('waittask specs', function () {
const {page, puppeteer} = getTestState(); const {page, puppeteer} = getTestState();
let error!: Error; let error!: Error;
await page.waitForFunction('false', {timeout: 10}).catch(error_ => { await page
return (error = error_); .waitForFunction(
}); () => {
return false;
},
{timeout: 10}
)
.catch(error_ => {
return (error = error_);
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain('waiting for function failed: timeout'); expect(error?.message).toContain('Waiting failed: 10ms exceeded');
}); });
it('should respect default timeout', async () => { it('should respect default timeout', async () => {
const {page, puppeteer} = getTestState(); const {page, puppeteer} = getTestState();
page.setDefaultTimeout(1); page.setDefaultTimeout(1);
let error!: Error; let error!: Error;
await page.waitForFunction('false').catch(error_ => { await page
return (error = error_); .waitForFunction(() => {
}); return false;
})
.catch(error_ => {
return (error = error_);
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain('waiting for function failed: timeout'); expect(error?.message).toContain('Waiting failed: 1ms exceeded');
}); });
it('should disable timeout when its set to 0', async () => { it('should disable timeout when its set to 0', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -341,7 +297,9 @@ describe('waittask specs', function () {
let fooFound = false; let fooFound = false;
const waitForFunction = page const waitForFunction = page
.waitForFunction('globalThis.__FOO === 1') .waitForFunction(() => {
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
})
.then(() => { .then(() => {
return (fooFound = true); return (fooFound = true);
}); });
@ -464,21 +422,18 @@ describe('waittask specs', function () {
await watchdog; await watchdog;
}); });
it( it('Page.waitForSelector is shortcut for main frame', async () => {
'Page.waitForSelector is shortcut for main frame', const {page, server} = getTestState();
async () => {
const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await attachFrame(page, 'frame1', server.EMPTY_PAGE); await attachFrame(page, 'frame1', server.EMPTY_PAGE);
const otherFrame = page.frames()[1]!; const otherFrame = page.frames()[1]!;
const watchdog = page.waitForSelector('div'); const watchdog = page.waitForSelector('div');
await otherFrame.evaluate(addElement, 'div'); await otherFrame.evaluate(addElement, 'div');
await page.evaluate(addElement, 'div'); await page.evaluate(addElement, 'div');
const eHandle = await watchdog; const eHandle = await watchdog;
expect(eHandle?.frame).toBe(page.mainFrame()); expect(eHandle?.frame).toBe(page.mainFrame());
} });
);
it('should run in specified frame', async () => { it('should run in specified frame', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -525,113 +480,186 @@ describe('waittask specs', function () {
await waitForSelector; await waitForSelector;
expect(boxFound).toBe(true); expect(boxFound).toBe(true);
}); });
it('should wait for visible', async () => { it('should wait for element to be visible (display)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divFound = false; const promise = page.waitForSelector('div', {visible: true});
const waitForSelector = page await page.setContent('<div style="display: none">text</div>');
.waitForSelector('div', {visible: true}) const element = await page.evaluateHandle(() => {
.then(() => { return document.getElementsByTagName('div')[0]!;
return (divFound = true);
});
await page.setContent(
`<div style='display: none; visibility: hidden;'>1</div>`
);
expect(divFound).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.style.removeProperty('display');
}); });
expect(divFound).toBe(false); await expect(
await page.evaluate(() => { Promise.race([promise, createTimeout(40)])
return document ).resolves.toBeFalsy();
.querySelector('div') await element.evaluate(e => {
?.style.removeProperty('visibility'); e.style.removeProperty('display');
}); });
expect(await waitForSelector).toBe(true); await expect(promise).resolves.toBeTruthy();
expect(divFound).toBe(true);
}); });
it('should wait for visible recursively', async () => { it('should wait for element to be visible (visibility)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divVisible = false; const promise = page.waitForSelector('div', {visible: true});
const waitForSelector = page await page.setContent('<div style="visibility: hidden">text</div>');
.waitForSelector('div#inner', {visible: true}) const element = await page.evaluateHandle(() => {
.then(() => { return document.getElementsByTagName('div')[0]!;
return (divVisible = true); });
}); await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('visibility', 'collapse');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.removeProperty('visibility');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be visible (bounding box)', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div', {visible: true});
await page.setContent('<div style="width: 0">text</div>');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('height', '0');
e.style.removeProperty('width');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('position', 'absolute');
e.style.setProperty('right', '100vw');
e.style.removeProperty('height');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('left', '100vw');
e.style.removeProperty('right');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('top', '100vh');
e.style.removeProperty('left');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('bottom', '100vh');
e.style.removeProperty('top');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
// Just peeking
e.style.setProperty('bottom', '99vh');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be visible recursively', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div#inner', {
visible: true,
});
await page.setContent( await page.setContent(
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>` `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
); );
expect(divVisible).toBe(false); const element = await page.evaluateHandle(() => {
await page.evaluate(() => { return document.getElementsByTagName('div')[0]!;
return document.querySelector('div')?.style.removeProperty('display');
}); });
expect(divVisible).toBe(false); await expect(
await page.evaluate(() => { Promise.race([promise, createTimeout(40)])
return document ).resolves.toBeFalsy();
.querySelector('div') await element.evaluate(e => {
?.style.removeProperty('visibility'); return e.style.removeProperty('display');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divVisible).toBe(true); Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.removeProperty('visibility');
});
await expect(promise).resolves.toBeTruthy();
}); });
it('hidden should wait for visibility: hidden', async () => { it('should wait for element to be hidden (visibility)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.then(() => {
return (divHidden = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.setProperty('visibility', 'hidden');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divHidden).toBe(true); Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.setProperty('visibility', 'hidden');
});
await expect(promise).resolves.toBeTruthy();
}); });
it('hidden should wait for display: none', async () => { it('should wait for element to be hidden (display)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.then(() => {
return (divHidden = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.setProperty('display', 'none');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divHidden).toBe(true); Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.setProperty('display', 'none');
});
await expect(promise).resolves.toBeTruthy();
}); });
it('hidden should wait for removal', async () => { it('should wait for element to be hidden (bounding box)', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div></div>`); const promise = page.waitForSelector('div', {hidden: true});
let divRemoved = false; await page.setContent('<div>text</div>');
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.then(() => {
return (divRemoved = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.remove();
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divRemoved).toBe(true); Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('height', '0');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be hidden (removal)', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div>text</div>`);
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
await expect(
Promise.race([promise, createTimeout(40, true)])
).resolves.toBeTruthy();
await element.evaluate(e => {
e.remove();
});
await expect(promise).resolves.toBeFalsy();
}); });
it('should return null if waiting to hide non-existing element', async () => { it('should return null if waiting to hide non-existing element', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -650,13 +678,13 @@ describe('waittask specs', function () {
}); });
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain( expect(error?.message).toContain(
'waiting for selector `div` failed: timeout' 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
); );
}); });
it('should have an error message specifically for awaiting an element to be hidden', async () => { it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div></div>`); await page.setContent(`<div>text</div>`);
let error!: Error; let error!: Error;
await page await page
.waitForSelector('div', {hidden: true, timeout: 10}) .waitForSelector('div', {hidden: true, timeout: 10})
@ -665,7 +693,7 @@ describe('waittask specs', function () {
}); });
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error?.message).toContain( expect(error?.message).toContain(
'waiting for selector `div` to be hidden failed: timeout' 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
); );
}); });
@ -701,9 +729,11 @@ describe('waittask specs', function () {
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => { await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error?.stack).toContain('waiting for selector `.zombo` failed'); expect(error?.stack).toContain(
'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
);
// The extension is ts here as Mocha maps back via sourcemaps. // The extension is ts here as Mocha maps back via sourcemaps.
expect(error?.stack).toContain('waittask.spec.ts'); expect(error?.stack).toContain('WaitTask.ts');
}); });
}); });
@ -733,9 +763,7 @@ describe('waittask specs', function () {
return (error = error_); return (error = error_);
}); });
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain( expect(error?.message).toContain('Waiting failed: 10ms exceeded');
'waiting for selector `.//div` failed: timeout 10ms exceeded'
);
}); });
it('should run in specified frame', async () => { it('should run in specified frame', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -772,7 +800,7 @@ describe('waittask specs', function () {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForXPath = page const waitForXPath = page
.waitForXPath('//div', {hidden: true}) .waitForXPath('//div', {hidden: true})
.then(() => { .then(() => {

View file

@ -18,14 +18,13 @@ import expect from 'expect';
import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js'; import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js'; import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js';
import { import {
describeFailsFirefox,
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {waitEvent} from './utils.js'; import {waitEvent} from './utils.js';
describeFailsFirefox('Workers', function () { describe('Workers', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
it('Page.workers', async () => { it('Page.workers', async () => {

View file

@ -9,7 +9,8 @@
}, },
"include": ["src"], "include": ["src"],
"references": [ "references": [
{"path": "../tsconfig.lib.json"}, {"path": "../src/tsconfig.cjs.json"},
{"path": "../utils/testserver/tsconfig.json"} {"path": "../utils/testserver/tsconfig.json"},
{"path": "../utils/mochaRunner/tsconfig.json"}
] ]
} }

View file

@ -0,0 +1,43 @@
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('./test/TestSuites.json', 'utf-8'));
/**
* @param {string} platform
* @returns {string}
*/
function mapPlatform(platform) {
switch (platform) {
case 'linux':
return 'ubuntu-latest';
case 'win32':
return 'windows-latest';
case 'darwin':
return 'macos-latest';
default:
throw new Error('Unsupported platform');
}
}
const result = [];
for (const suite of data.testSuites) {
for (const platform of suite.platforms) {
if (platform === 'linux' && suite.id !== 'firefox-bidi') {
for (const node of [14, 16, 18]) {
result.push(`- name: ${suite.id}
machine: ${mapPlatform(platform)}
xvfb: true
node: ${node}
suite: ${suite.id}`);
}
} else {
result.push(`- name: ${suite.id}
machine: ${mapPlatform(platform)}
xvfb: ${platform === 'linux'}
node: 18
suite: ${suite.id}`);
}
}
}
console.log(result.join('\n'));

View file

@ -6,7 +6,7 @@ import {sync as glob} from 'glob';
import path from 'path'; import path from 'path';
import {job} from './internal/job.js'; import {job} from './internal/job.js';
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util']; const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util', 'api'];
(async () => { (async () => {
await job('', async ({outputs}) => { await job('', async ({outputs}) => {
@ -36,7 +36,7 @@ const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
outdir: tmp, outdir: tmp,
format: 'cjs', format: 'cjs',
platform: 'browser', platform: 'browser',
target: 'ES2019', target: 'ES2022',
}); });
const baseName = path.basename(input); const baseName = path.basename(input);
const content = await readFile( const content = await readFile(

Some files were not shown because too many files have changed in this diff Show more