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
Родитель b306cf161f
Коммит d115e24d80
109 изменённых файлов: 7883 добавлений и 4919 удалений

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

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

2
remote/.gitignore поставляемый
Просмотреть файл

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

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

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

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

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

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

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

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

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

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

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

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

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

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

22
remote/test/puppeteer/package-lock.json сгенерированный
Просмотреть файл

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

16
remote/test/puppeteer/src/compat.d.ts поставляемый
Просмотреть файл

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 8.3 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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