summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/common
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/common')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts114
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts177
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts120
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts77
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts15
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts113
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts207
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts128
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts1552
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts124
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts185
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts253
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts92
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts49
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts76
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts37
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts38
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts217
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts31
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts11
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts205
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts52
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts78
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts29
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts20
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts45
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts671
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts50
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts275
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts35
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts40
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts14
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts225
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts447
37 files changed, 6004 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
new file mode 100644
index 0000000000..217e53bedd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts
@@ -0,0 +1,114 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Browser} from '../api/Browser.js';
+import {_connectToBiDiBrowser} from '../bidi/BrowserConnector.js';
+import {_connectToCdpBrowser} from '../cdp/BrowserConnector.js';
+import {isNode} from '../environment.js';
+import {assert} from '../util/assert.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import type {ConnectionTransport} from './ConnectionTransport.js';
+import type {ConnectOptions} from './ConnectOptions.js';
+import type {BrowserConnectOptions} from './ConnectOptions.js';
+import {getFetch} from './fetch.js';
+
+const getWebSocketTransportClass = async () => {
+ return isNode
+ ? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport
+ : (await import('../common/BrowserWebSocketTransport.js'))
+ .BrowserWebSocketTransport;
+};
+
+/**
+ * Users should never call this directly; it's called when calling
+ * `puppeteer.connect`. This method attaches Puppeteer to an existing browser instance.
+ *
+ * @internal
+ */
+export async function _connectToBrowser(
+ options: ConnectOptions
+): Promise<Browser> {
+ const {connectionTransport, endpointUrl} =
+ await getConnectionTransport(options);
+
+ if (options.protocol === 'webDriverBiDi') {
+ const bidiBrowser = await _connectToBiDiBrowser(
+ connectionTransport,
+ endpointUrl,
+ options
+ );
+ return bidiBrowser;
+ } else {
+ const cdpBrowser = await _connectToCdpBrowser(
+ connectionTransport,
+ endpointUrl,
+ options
+ );
+ return cdpBrowser;
+ }
+}
+
+/**
+ * Establishes a websocket connection by given options and returns both transport and
+ * endpoint url the transport is connected to.
+ */
+async function getConnectionTransport(
+ options: BrowserConnectOptions & ConnectOptions
+): Promise<{connectionTransport: ConnectionTransport; endpointUrl: string}> {
+ const {browserWSEndpoint, browserURL, transport, headers = {}} = options;
+
+ assert(
+ Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) ===
+ 1,
+ 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
+ );
+
+ if (transport) {
+ return {connectionTransport: transport, endpointUrl: ''};
+ } else if (browserWSEndpoint) {
+ const WebSocketClass = await getWebSocketTransportClass();
+ const connectionTransport: ConnectionTransport =
+ await WebSocketClass.create(browserWSEndpoint, headers);
+ return {
+ connectionTransport: connectionTransport,
+ endpointUrl: browserWSEndpoint,
+ };
+ } else if (browserURL) {
+ const connectionURL = await getWSEndpoint(browserURL);
+ const WebSocketClass = await getWebSocketTransportClass();
+ const connectionTransport: ConnectionTransport =
+ await WebSocketClass.create(connectionURL);
+ return {
+ connectionTransport: connectionTransport,
+ endpointUrl: connectionURL,
+ };
+ }
+ throw new Error('Invalid connection options');
+}
+
+async function getWSEndpoint(browserURL: string): Promise<string> {
+ const endpointURL = new URL('/json/version', browserURL);
+
+ const fetch = await getFetch();
+ try {
+ const result = await fetch(endpointURL.toString(), {
+ method: 'GET',
+ });
+ if (!result.ok) {
+ throw new Error(`HTTP ${result.statusText}`);
+ }
+ const data = await result.json();
+ return data.webSocketDebuggerUrl;
+ } catch (error) {
+ if (isErrorLike(error)) {
+ error.message =
+ `Failed to fetch browser webSocket URL from ${endpointURL}: ` +
+ error.message;
+ }
+ throw error;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts
new file mode 100644
index 0000000000..cc0f81cb06
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {ConnectionTransport} from './ConnectionTransport.js';
+
+/**
+ * @internal
+ */
+export class BrowserWebSocketTransport implements ConnectionTransport {
+ static create(url: string): Promise<BrowserWebSocketTransport> {
+ return new Promise((resolve, reject) => {
+ const ws = new WebSocket(url);
+
+ ws.addEventListener('open', () => {
+ return resolve(new BrowserWebSocketTransport(ws));
+ });
+ ws.addEventListener('error', reject);
+ });
+ }
+
+ #ws: WebSocket;
+ onmessage?: (message: string) => void;
+ onclose?: () => void;
+
+ constructor(ws: WebSocket) {
+ this.#ws = ws;
+ this.#ws.addEventListener('message', event => {
+ if (this.onmessage) {
+ this.onmessage.call(null, event.data);
+ }
+ });
+ this.#ws.addEventListener('close', () => {
+ if (this.onclose) {
+ this.onclose.call(null);
+ }
+ });
+ // Silently ignore all errors - we don't know what to do with them.
+ this.#ws.addEventListener('error', () => {});
+ }
+
+ send(message: string): void {
+ this.#ws.send(message);
+ }
+
+ close(): void {
+ this.#ws.close();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts
new file mode 100644
index 0000000000..ea9f3d5abb
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/CallbackRegistry.ts
@@ -0,0 +1,177 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {Deferred} from '../util/Deferred.js';
+import {rewriteError} from '../util/ErrorLike.js';
+
+import {ProtocolError, TargetCloseError} from './Errors.js';
+import {debugError} from './util.js';
+
+/**
+ * Manages callbacks and their IDs for the protocol request/response communication.
+ *
+ * @internal
+ */
+export class CallbackRegistry {
+ #callbacks = new Map<number, Callback>();
+ #idGenerator = createIncrementalIdGenerator();
+
+ create(
+ label: string,
+ timeout: number | undefined,
+ request: (id: number) => void
+ ): Promise<unknown> {
+ const callback = new Callback(this.#idGenerator(), label, timeout);
+ this.#callbacks.set(callback.id, callback);
+ try {
+ request(callback.id);
+ } catch (error) {
+ // We still throw sync errors synchronously and clean up the scheduled
+ // callback.
+ callback.promise
+ .valueOrThrow()
+ .catch(debugError)
+ .finally(() => {
+ this.#callbacks.delete(callback.id);
+ });
+ callback.reject(error as Error);
+ throw error;
+ }
+ // Must only have sync code up until here.
+ return callback.promise.valueOrThrow().finally(() => {
+ this.#callbacks.delete(callback.id);
+ });
+ }
+
+ reject(id: number, message: string, originalMessage?: string): void {
+ const callback = this.#callbacks.get(id);
+ if (!callback) {
+ return;
+ }
+ this._reject(callback, message, originalMessage);
+ }
+
+ _reject(
+ callback: Callback,
+ errorMessage: string | ProtocolError,
+ originalMessage?: string
+ ): void {
+ let error: ProtocolError;
+ let message: string;
+ if (errorMessage instanceof ProtocolError) {
+ error = errorMessage;
+ error.cause = callback.error;
+ message = errorMessage.message;
+ } else {
+ error = callback.error;
+ message = errorMessage;
+ }
+
+ callback.reject(
+ rewriteError(
+ error,
+ `Protocol error (${callback.label}): ${message}`,
+ originalMessage
+ )
+ );
+ }
+
+ resolve(id: number, value: unknown): void {
+ const callback = this.#callbacks.get(id);
+ if (!callback) {
+ return;
+ }
+ callback.resolve(value);
+ }
+
+ clear(): void {
+ for (const callback of this.#callbacks.values()) {
+ // TODO: probably we can accept error messages as params.
+ this._reject(callback, new TargetCloseError('Target closed'));
+ }
+ this.#callbacks.clear();
+ }
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ const result: Error[] = [];
+ for (const callback of this.#callbacks.values()) {
+ result.push(
+ new Error(`${callback.label} timed out. Trace: ${callback.error.stack}`)
+ );
+ }
+ return result;
+ }
+}
+/**
+ * @internal
+ */
+
+export class Callback {
+ #id: number;
+ #error = new ProtocolError();
+ #deferred = Deferred.create<unknown>();
+ #timer?: ReturnType<typeof setTimeout>;
+ #label: string;
+
+ constructor(id: number, label: string, timeout?: number) {
+ this.#id = id;
+ this.#label = label;
+ if (timeout) {
+ this.#timer = setTimeout(() => {
+ this.#deferred.reject(
+ rewriteError(
+ this.#error,
+ `${label} timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`
+ )
+ );
+ }, timeout);
+ }
+ }
+
+ resolve(value: unknown): void {
+ clearTimeout(this.#timer);
+ this.#deferred.resolve(value);
+ }
+
+ reject(error: Error): void {
+ clearTimeout(this.#timer);
+ this.#deferred.reject(error);
+ }
+
+ get id(): number {
+ return this.#id;
+ }
+
+ get promise(): Deferred<unknown> {
+ return this.#deferred;
+ }
+
+ get error(): ProtocolError {
+ return this.#error;
+ }
+
+ get label(): string {
+ return this.#label;
+ }
+}
+
+/**
+ * @internal
+ */
+export function createIncrementalIdGenerator(): GetIdFn {
+ let id = 0;
+ return (): number => {
+ return ++id;
+ };
+}
+
+/**
+ * @internal
+ */
+export type GetIdFn = () => number;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
new file mode 100644
index 0000000000..c64d109a7c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Product} from './Product.js';
+
+/**
+ * Defines experiment options for Puppeteer.
+ *
+ * See individual properties for more information.
+ *
+ * @public
+ */
+export type ExperimentsConfiguration = Record<string, never>;
+
+/**
+ * Defines options to configure Puppeteer's behavior during installation and
+ * runtime.
+ *
+ * See individual properties for more information.
+ *
+ * @public
+ */
+export interface Configuration {
+ /**
+ * Specifies a certain version of the browser you'd like Puppeteer to use.
+ *
+ * Can be overridden by `PUPPETEER_BROWSER_REVISION`.
+ *
+ * See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path
+ * is inferred.
+ *
+ * @defaultValue A compatible-revision of the browser.
+ */
+ browserRevision?: string;
+ /**
+ * Defines the directory to be used by Puppeteer for caching.
+ *
+ * Can be overridden by `PUPPETEER_CACHE_DIR`.
+ *
+ * @defaultValue `path.join(os.homedir(), '.cache', 'puppeteer')`
+ */
+ cacheDirectory?: string;
+ /**
+ * Specifies the URL prefix that is used to download the browser.
+ *
+ * Can be overridden by `PUPPETEER_DOWNLOAD_BASE_URL`.
+ *
+ * @remarks
+ * This must include the protocol and may even need a path prefix.
+ *
+ * @defaultValue Either https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or
+ * https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central,
+ * depending on the product.
+ */
+ downloadBaseUrl?: string;
+ /**
+ * Specifies the path for the downloads folder.
+ *
+ * Can be overridden by `PUPPETEER_DOWNLOAD_PATH`.
+ *
+ * @defaultValue `<cacheDirectory>`
+ */
+ downloadPath?: string;
+ /**
+ * Specifies an executable path to be used in
+ * {@link PuppeteerNode.launch | puppeteer.launch}.
+ *
+ * Can be overridden by `PUPPETEER_EXECUTABLE_PATH`.
+ *
+ * @defaultValue **Auto-computed.**
+ */
+ executablePath?: string;
+ /**
+ * Specifies which browser you'd like Puppeteer to use.
+ *
+ * Can be overridden by `PUPPETEER_PRODUCT`.
+ *
+ * @defaultValue `chrome`
+ */
+ defaultProduct?: Product;
+ /**
+ * Defines the directory to be used by Puppeteer for creating temporary files.
+ *
+ * Can be overridden by `PUPPETEER_TMP_DIR`.
+ *
+ * @defaultValue `os.tmpdir()`
+ */
+ temporaryDirectory?: string;
+ /**
+ * Tells Puppeteer to not download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_DOWNLOAD`.
+ */
+ skipDownload?: boolean;
+ /**
+ * Tells Puppeteer to not Chrome download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_CHROME_DOWNLOAD`.
+ */
+ skipChromeDownload?: boolean;
+ /**
+ * Tells Puppeteer to not chrome-headless-shell download during installation.
+ *
+ * Can be overridden by `PUPPETEER_SKIP_CHROME_HEADLESSS_HELL_DOWNLOAD`.
+ */
+ skipChromeHeadlessShellDownload?: boolean;
+ /**
+ * Tells Puppeteer to log at the given level.
+ *
+ * @defaultValue `warn`
+ */
+ logLevel?: 'silent' | 'error' | 'warn';
+ /**
+ * Defines experimental options for Puppeteer.
+ */
+ experiments?: ExperimentsConfiguration;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts
new file mode 100644
index 0000000000..ce46585162
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectOptions.ts
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {
+ IsPageTargetCallback,
+ TargetFilterCallback,
+} from '../api/Browser.js';
+
+import type {ConnectionTransport} from './ConnectionTransport.js';
+import type {Viewport} from './Viewport.js';
+
+/**
+ * @public
+ */
+export type ProtocolType = 'cdp' | 'webDriverBiDi';
+
+/**
+ * Generic browser options that can be passed when launching any browser or when
+ * connecting to an existing browser instance.
+ * @public
+ */
+export interface BrowserConnectOptions {
+ /**
+ * Whether to ignore HTTPS errors during navigation.
+ * @defaultValue `false`
+ */
+ ignoreHTTPSErrors?: boolean;
+ /**
+ * Sets the viewport for each page.
+ *
+ * @defaultValue '\{width: 800, height: 600\}'
+ */
+ defaultViewport?: Viewport | null;
+ /**
+ * Slows down Puppeteer operations by the specified amount of milliseconds to
+ * aid debugging.
+ */
+ slowMo?: number;
+ /**
+ * Callback to decide if Puppeteer should connect to a given target or not.
+ */
+ targetFilter?: TargetFilterCallback;
+ /**
+ * @internal
+ */
+ _isPageTarget?: IsPageTargetCallback;
+
+ /**
+ * @defaultValue 'cdp'
+ * @public
+ */
+ protocol?: ProtocolType;
+ /**
+ * Timeout setting for individual protocol (CDP) calls.
+ *
+ * @defaultValue `180_000`
+ */
+ protocolTimeout?: number;
+}
+
+/**
+ * @public
+ */
+export interface ConnectOptions extends BrowserConnectOptions {
+ browserWSEndpoint?: string;
+ browserURL?: string;
+ transport?: ConnectionTransport;
+ /**
+ * Headers to use for the web socket connection.
+ * @remarks
+ * Only works in the Node.js environment.
+ */
+ headers?: Record<string, string>;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts
new file mode 100644
index 0000000000..ff36a2557a
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface ConnectionTransport {
+ send(message: string): void;
+ close(): void;
+ onmessage?: (message: string) => void;
+ onclose?: () => void;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
new file mode 100644
index 0000000000..85d2db9f75
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+
+/**
+ * @public
+ */
+export interface ConsoleMessageLocation {
+ /**
+ * URL of the resource if known or `undefined` otherwise.
+ */
+ url?: string;
+
+ /**
+ * 0-based line number in the resource if known or `undefined` otherwise.
+ */
+ lineNumber?: number;
+
+ /**
+ * 0-based column number in the resource if known or `undefined` otherwise.
+ */
+ columnNumber?: number;
+}
+
+/**
+ * The supported types for console messages.
+ * @public
+ */
+export type ConsoleMessageType =
+ | 'log'
+ | 'debug'
+ | 'info'
+ | 'error'
+ | 'warning'
+ | 'dir'
+ | 'dirxml'
+ | 'table'
+ | 'trace'
+ | 'clear'
+ | 'startGroup'
+ | 'startGroupCollapsed'
+ | 'endGroup'
+ | 'assert'
+ | 'profile'
+ | 'profileEnd'
+ | 'count'
+ | 'timeEnd'
+ | 'verbose';
+
+/**
+ * ConsoleMessage objects are dispatched by page via the 'console' event.
+ * @public
+ */
+export class ConsoleMessage {
+ #type: ConsoleMessageType;
+ #text: string;
+ #args: JSHandle[];
+ #stackTraceLocations: ConsoleMessageLocation[];
+
+ /**
+ * @public
+ */
+ constructor(
+ type: ConsoleMessageType,
+ text: string,
+ args: JSHandle[],
+ stackTraceLocations: ConsoleMessageLocation[]
+ ) {
+ this.#type = type;
+ this.#text = text;
+ this.#args = args;
+ this.#stackTraceLocations = stackTraceLocations;
+ }
+
+ /**
+ * The type of the console message.
+ */
+ type(): ConsoleMessageType {
+ return this.#type;
+ }
+
+ /**
+ * The text of the console message.
+ */
+ text(): string {
+ return this.#text;
+ }
+
+ /**
+ * An array of arguments passed to the console.
+ */
+ args(): JSHandle[] {
+ return this.#args;
+ }
+
+ /**
+ * The location of the console message.
+ */
+ location(): ConsoleMessageLocation {
+ return this.#stackTraceLocations[0] ?? {};
+ }
+
+ /**
+ * The array of locations on the stack of the console message.
+ */
+ stackTrace(): ConsoleMessageLocation[] {
+ return this.#stackTraceLocations;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts
new file mode 100644
index 0000000000..33e5f889c1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts
@@ -0,0 +1,207 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type PuppeteerUtil from '../injected/injected.js';
+import {assert} from '../util/assert.js';
+import {interpolateFunction, stringifyFunction} from '../util/Function.js';
+
+import {
+ QueryHandler,
+ type QuerySelector,
+ type QuerySelectorAll,
+} from './QueryHandler.js';
+import {scriptInjector} from './ScriptInjector.js';
+
+/**
+ * @public
+ */
+export interface CustomQueryHandler {
+ /**
+ * Searches for a {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
+ */
+ queryOne?: (node: Node, selector: string) => Node | null;
+ /**
+ * Searches for some {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Nodes} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
+ */
+ queryAll?: (node: Node, selector: string) => Iterable<Node>;
+}
+
+/**
+ * The registry of {@link CustomQueryHandler | custom query handlers}.
+ *
+ * @example
+ *
+ * ```ts
+ * Puppeteer.customQueryHandlers.register('lit', { … });
+ * const aHandle = await page.$('lit/…');
+ * ```
+ *
+ * @internal
+ */
+export class CustomQueryHandlerRegistry {
+ #handlers = new Map<
+ string,
+ [registerScript: string, Handler: typeof QueryHandler]
+ >();
+
+ get(name: string): typeof QueryHandler | undefined {
+ const handler = this.#handlers.get(name);
+ return handler ? handler[1] : undefined;
+ }
+
+ /**
+ * Registers a {@link CustomQueryHandler | custom query handler}.
+ *
+ * @remarks
+ * After registration, the handler can be used everywhere where a selector is
+ * expected by prepending the selection string with `<name>/`. The name is
+ * only allowed to consist of lower- and upper case latin letters.
+ *
+ * @example
+ *
+ * ```ts
+ * Puppeteer.customQueryHandlers.register('lit', { … });
+ * const aHandle = await page.$('lit/…');
+ * ```
+ *
+ * @param name - Name to register under.
+ * @param queryHandler - {@link CustomQueryHandler | Custom query handler} to
+ * register.
+ */
+ register(name: string, handler: CustomQueryHandler): void {
+ assert(
+ !this.#handlers.has(name),
+ `Cannot register over existing handler: ${name}`
+ );
+ assert(
+ /^[a-zA-Z]+$/.test(name),
+ `Custom query handler names may only contain [a-zA-Z]`
+ );
+ assert(
+ handler.queryAll || handler.queryOne,
+ `At least one query method must be implemented.`
+ );
+
+ const Handler = class extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = interpolateFunction(
+ (node, selector, PuppeteerUtil) => {
+ return PuppeteerUtil.customQuerySelectors
+ .get(PLACEHOLDER('name'))!
+ .querySelectorAll(node, selector);
+ },
+ {name: JSON.stringify(name)}
+ );
+ static override querySelector: QuerySelector = interpolateFunction(
+ (node, selector, PuppeteerUtil) => {
+ return PuppeteerUtil.customQuerySelectors
+ .get(PLACEHOLDER('name'))!
+ .querySelector(node, selector);
+ },
+ {name: JSON.stringify(name)}
+ );
+ };
+ const registerScript = interpolateFunction(
+ (PuppeteerUtil: PuppeteerUtil) => {
+ PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), {
+ queryAll: PLACEHOLDER('queryAll'),
+ queryOne: PLACEHOLDER('queryOne'),
+ });
+ },
+ {
+ name: JSON.stringify(name),
+ queryAll: handler.queryAll
+ ? stringifyFunction(handler.queryAll)
+ : String(undefined),
+ queryOne: handler.queryOne
+ ? stringifyFunction(handler.queryOne)
+ : String(undefined),
+ }
+ ).toString();
+
+ this.#handlers.set(name, [registerScript, Handler]);
+ scriptInjector.append(registerScript);
+ }
+
+ /**
+ * Unregisters the {@link CustomQueryHandler | custom query handler} for the
+ * given name.
+ *
+ * @throws `Error` if there is no handler under the given name.
+ */
+ unregister(name: string): void {
+ const handler = this.#handlers.get(name);
+ if (!handler) {
+ throw new Error(`Cannot unregister unknown handler: ${name}`);
+ }
+ scriptInjector.pop(handler[0]);
+ this.#handlers.delete(name);
+ }
+
+ /**
+ * Gets the names of all {@link CustomQueryHandler | custom query handlers}.
+ */
+ names(): string[] {
+ return [...this.#handlers.keys()];
+ }
+
+ /**
+ * Unregisters all custom query handlers.
+ */
+ clear(): void {
+ for (const [registerScript] of this.#handlers) {
+ scriptInjector.pop(registerScript);
+ }
+ this.#handlers.clear();
+ }
+}
+
+/**
+ * @internal
+ */
+export const customQueryHandlers = new CustomQueryHandlerRegistry();
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.registerCustomQueryHandler}
+ *
+ * @public
+ */
+export function registerCustomQueryHandler(
+ name: string,
+ handler: CustomQueryHandler
+): void {
+ customQueryHandlers.register(name, handler);
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.unregisterCustomQueryHandler}
+ *
+ * @public
+ */
+export function unregisterCustomQueryHandler(name: string): void {
+ customQueryHandlers.unregister(name);
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.customQueryHandlerNames}
+ *
+ * @public
+ */
+export function customQueryHandlerNames(): string[] {
+ return customQueryHandlers.names();
+}
+
+/**
+ * @deprecated Import {@link Puppeteer} and use the static method
+ * {@link Puppeteer.clearCustomQueryHandlers}
+ *
+ * @public
+ */
+export function clearCustomQueryHandlers(): void {
+ customQueryHandlers.clear();
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts
new file mode 100644
index 0000000000..06ac9f58f9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type Debug from 'debug';
+
+import {isNode} from '../environment.js';
+
+declare global {
+ // eslint-disable-next-line no-var
+ var __PUPPETEER_DEBUG: string;
+}
+
+/**
+ * @internal
+ */
+let debugModule: typeof Debug | null = null;
+/**
+ * @internal
+ */
+export async function importDebug(): Promise<typeof Debug> {
+ if (!debugModule) {
+ debugModule = (await import('debug')).default;
+ }
+ return debugModule;
+}
+
+/**
+ * A debug function that can be used in any environment.
+ *
+ * @remarks
+ * If used in Node, it falls back to the
+ * {@link https://www.npmjs.com/package/debug | debug module}. In the browser it
+ * uses `console.log`.
+ *
+ * In Node, use the `DEBUG` environment variable to control logging:
+ *
+ * ```
+ * DEBUG=* // logs all channels
+ * DEBUG=foo // logs the `foo` channel
+ * DEBUG=foo* // logs any channels starting with `foo`
+ * ```
+ *
+ * In the browser, set `window.__PUPPETEER_DEBUG` to a string:
+ *
+ * ```
+ * window.__PUPPETEER_DEBUG='*'; // logs all channels
+ * window.__PUPPETEER_DEBUG='foo'; // logs the `foo` channel
+ * window.__PUPPETEER_DEBUG='foo*'; // logs any channels starting with `foo`
+ * ```
+ *
+ * @example
+ *
+ * ```
+ * const log = debug('Page');
+ *
+ * log('new page created')
+ * // logs "Page: new page created"
+ * ```
+ *
+ * @param prefix - this will be prefixed to each log.
+ * @returns a function that can be called to log to that debug channel.
+ *
+ * @internal
+ */
+export const debug = (prefix: string): ((...args: unknown[]) => void) => {
+ if (isNode) {
+ return async (...logArgs: unknown[]) => {
+ if (captureLogs) {
+ capturedLogs.push(prefix + logArgs);
+ }
+ (await importDebug())(prefix)(logArgs);
+ };
+ }
+
+ return (...logArgs: unknown[]): void => {
+ const debugLevel = (globalThis as any).__PUPPETEER_DEBUG;
+ if (!debugLevel) {
+ return;
+ }
+
+ const everythingShouldBeLogged = debugLevel === '*';
+
+ const prefixMatchesDebugLevel =
+ everythingShouldBeLogged ||
+ /**
+ * If the debug level is `foo*`, that means we match any prefix that
+ * starts with `foo`. If the level is `foo`, we match only the prefix
+ * `foo`.
+ */
+ (debugLevel.endsWith('*')
+ ? prefix.startsWith(debugLevel)
+ : prefix === debugLevel);
+
+ if (!prefixMatchesDebugLevel) {
+ return;
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`${prefix}:`, ...logArgs);
+ };
+};
+
+/**
+ * @internal
+ */
+let capturedLogs: string[] = [];
+/**
+ * @internal
+ */
+let captureLogs = false;
+
+/**
+ * @internal
+ */
+export function setLogCapture(value: boolean): void {
+ capturedLogs = [];
+ captureLogs = value;
+}
+
+/**
+ * @internal
+ */
+export function getCapturedLogs(): string[] {
+ return capturedLogs;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
new file mode 100644
index 0000000000..dbf5c13c95
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts
@@ -0,0 +1,1552 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Viewport} from './Viewport.js';
+
+/**
+ * @public
+ */
+export interface Device {
+ userAgent: string;
+ viewport: Viewport;
+}
+
+const knownDevices = [
+ {
+ name: 'Blackberry PlayBook',
+ userAgent:
+ 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
+ viewport: {
+ width: 600,
+ height: 1024,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Blackberry PlayBook landscape',
+ userAgent:
+ 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+',
+ viewport: {
+ width: 1024,
+ height: 600,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'BlackBerry Z30',
+ userAgent:
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'BlackBerry Z30 landscape',
+ userAgent:
+ 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Note 3',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Note 3 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Note II',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Note II landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S III',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S III landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S8',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 740,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S8 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
+ viewport: {
+ width: 740,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy S9+',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
+ viewport: {
+ width: 320,
+ height: 658,
+ deviceScaleFactor: 4.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy S9+ landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
+ viewport: {
+ width: 658,
+ height: 320,
+ deviceScaleFactor: 4.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Galaxy Tab S4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
+ viewport: {
+ width: 712,
+ height: 1138,
+ deviceScaleFactor: 2.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Galaxy Tab S4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
+ viewport: {
+ width: 1138,
+ height: 712,
+ deviceScaleFactor: 2.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad (gen 6)',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad (gen 6) landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad (gen 7)',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 810,
+ height: 1080,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad (gen 7) landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1080,
+ height: 810,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 768,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 768,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1024,
+ height: 1366,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
+ viewport: {
+ width: 1366,
+ height: 1024,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPad Pro 11',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 834,
+ height: 1194,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPad Pro 11 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 1194,
+ height: 834,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 4',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
+ viewport: {
+ width: 320,
+ height: 480,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53',
+ viewport: {
+ width: 480,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 5',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 320,
+ height: 568,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 568,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 6',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 6 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 6 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 6 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 7',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 7 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 7 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 7 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 8',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 667,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 8 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 667,
+ height: 375,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 8 Plus',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 736,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 8 Plus landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 736,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone SE',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 320,
+ height: 568,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone SE landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
+ viewport: {
+ width: 568,
+ height: 320,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone X',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone X landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone XR',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 896,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone XR landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 896,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 828,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 828,
+ height: 414,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 414,
+ height: 896,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 11 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 896,
+ height: 414,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 428,
+ height: 926,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 926,
+ height: 428,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 12 Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 12 Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 390,
+ height: 844,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 844,
+ height: 390,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro Max',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 428,
+ height: 926,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Pro Max landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 926,
+ height: 428,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'iPhone 13 Mini',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 375,
+ height: 812,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'iPhone 13 Mini landscape',
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1',
+ viewport: {
+ width: 812,
+ height: 375,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'JioPhone 2',
+ userAgent:
+ 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
+ viewport: {
+ width: 240,
+ height: 320,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'JioPhone 2 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
+ viewport: {
+ width: 320,
+ height: 240,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Kindle Fire HDX',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
+ viewport: {
+ width: 800,
+ height: 1280,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Kindle Fire HDX landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true',
+ viewport: {
+ width: 1280,
+ height: 800,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'LG Optimus L70',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 384,
+ height: 640,
+ deviceScaleFactor: 1.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'LG Optimus L70 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 384,
+ deviceScaleFactor: 1.25,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 550',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 950',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 4,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Microsoft Lumia 950 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 4,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 10',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 800,
+ height: 1280,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 10 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 1280,
+ height: 800,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 384,
+ height: 640,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 384,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 5X',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 5X landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 6',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 6 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 6P',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 412,
+ height: 732,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 6P landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 732,
+ height: 412,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nexus 7',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 600,
+ height: 960,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nexus 7 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36',
+ viewport: {
+ width: 960,
+ height: 600,
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nokia Lumia 520',
+ userAgent:
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
+ viewport: {
+ width: 320,
+ height: 533,
+ deviceScaleFactor: 1.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nokia Lumia 520 landscape',
+ userAgent:
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
+ viewport: {
+ width: 533,
+ height: 320,
+ deviceScaleFactor: 1.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Nokia N9',
+ userAgent:
+ 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
+ viewport: {
+ width: 480,
+ height: 854,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Nokia N9 landscape',
+ userAgent:
+ 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13',
+ viewport: {
+ width: 854,
+ height: 480,
+ deviceScaleFactor: 1,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 2',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 411,
+ height: 731,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 2 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 731,
+ height: 411,
+ deviceScaleFactor: 2.625,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 2 XL',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 411,
+ height: 823,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 2 XL landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36',
+ viewport: {
+ width: 823,
+ height: 411,
+ deviceScaleFactor: 3.5,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 3',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
+ viewport: {
+ width: 393,
+ height: 786,
+ deviceScaleFactor: 2.75,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 3 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
+ viewport: {
+ width: 786,
+ height: 393,
+ deviceScaleFactor: 2.75,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
+ viewport: {
+ width: 353,
+ height: 745,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
+ viewport: {
+ width: 745,
+ height: 353,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 4a (5G)',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 353,
+ height: 745,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 4a (5G) landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 745,
+ height: 353,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Pixel 5',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 393,
+ height: 851,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Pixel 5 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 851,
+ height: 393,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+ {
+ name: 'Moto G4',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 360,
+ height: 640,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: false,
+ },
+ },
+ {
+ name: 'Moto G4 landscape',
+ userAgent:
+ 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36',
+ viewport: {
+ width: 640,
+ height: 360,
+ deviceScaleFactor: 3,
+ isMobile: true,
+ hasTouch: true,
+ isLandscape: true,
+ },
+ },
+] as const;
+
+const knownDevicesByName = {} as Record<
+ (typeof knownDevices)[number]['name'],
+ Device
+>;
+
+for (const device of knownDevices) {
+ knownDevicesByName[device.name] = device;
+}
+
+/**
+ * A list of devices to be used with {@link Page.emulate}.
+ *
+ * @example
+ *
+ * ```ts
+ * import {KnownDevices} from 'puppeteer';
+ * const iPhone = KnownDevices['iPhone 6'];
+ *
+ * (async () => {
+ * const browser = await puppeteer.launch();
+ * const page = await browser.newPage();
+ * await page.emulate(iPhone);
+ * await page.goto('https://www.google.com');
+ * // other actions...
+ * await browser.close();
+ * })();
+ * ```
+ *
+ * @public
+ */
+export const KnownDevices = Object.freeze(knownDevicesByName);
+
+/**
+ * @deprecated Import {@link KnownDevices}
+ *
+ * @public
+ */
+export const devices = KnownDevices;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
new file mode 100644
index 0000000000..8225d64f07
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts
@@ -0,0 +1,124 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @deprecated Do not use.
+ *
+ * @public
+ */
+export class CustomError extends Error {
+ /**
+ * @internal
+ */
+ constructor(message?: string) {
+ super(message);
+ this.name = this.constructor.name;
+ }
+
+ /**
+ * @internal
+ */
+ get [Symbol.toStringTag](): string {
+ return this.constructor.name;
+ }
+}
+
+/**
+ * TimeoutError is emitted whenever certain operations are terminated due to
+ * timeout.
+ *
+ * @remarks
+ * Example operations are {@link Page.waitForSelector | page.waitForSelector} or
+ * {@link PuppeteerNode.launch | puppeteer.launch}.
+ *
+ * @public
+ */
+export class TimeoutError extends CustomError {}
+
+/**
+ * ProtocolError is emitted whenever there is an error from the protocol.
+ *
+ * @public
+ */
+export class ProtocolError extends CustomError {
+ #code?: number;
+ #originalMessage = '';
+
+ set code(code: number | undefined) {
+ this.#code = code;
+ }
+ /**
+ * @readonly
+ * @public
+ */
+ get code(): number | undefined {
+ return this.#code;
+ }
+
+ set originalMessage(originalMessage: string) {
+ this.#originalMessage = originalMessage;
+ }
+ /**
+ * @readonly
+ * @public
+ */
+ get originalMessage(): string {
+ return this.#originalMessage;
+ }
+}
+
+/**
+ * Puppeteer will throw this error if a method is not
+ * supported by the currently used protocol
+ *
+ * @public
+ */
+export class UnsupportedOperation extends CustomError {}
+
+/**
+ * @internal
+ */
+export class TargetCloseError extends ProtocolError {}
+
+/**
+ * @deprecated Do not use.
+ *
+ * @public
+ */
+export interface PuppeteerErrors {
+ TimeoutError: typeof TimeoutError;
+ ProtocolError: typeof ProtocolError;
+}
+
+/**
+ * @deprecated Import error classes directly.
+ *
+ * Puppeteer methods might throw errors if they are unable to fulfill a request.
+ * For example, `page.waitForSelector(selector[, options])` might fail if the
+ * selector doesn't match any nodes during the given timeframe.
+ *
+ * For certain types of errors Puppeteer uses specific error classes. These
+ * classes are available via `puppeteer.errors`.
+ *
+ * @example
+ * An example of handling a timeout error:
+ *
+ * ```ts
+ * try {
+ * await page.waitForSelector('.foo');
+ * } catch (e) {
+ * if (e instanceof TimeoutError) {
+ * // Do something if this is a timeout.
+ * }
+ * }
+ * ```
+ *
+ * @public
+ */
+export const errors: PuppeteerErrors = Object.freeze({
+ TimeoutError,
+ ProtocolError,
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
new file mode 100644
index 0000000000..cf05ef6700
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.test.ts
@@ -0,0 +1,185 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {describe, it, beforeEach} from 'node:test';
+
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {EventEmitter} from './EventEmitter.js';
+
+describe('EventEmitter', () => {
+ let emitter: EventEmitter<Record<string, unknown>>;
+
+ beforeEach(() => {
+ emitter = new EventEmitter();
+ });
+
+ describe('on', () => {
+ const onTests = (methodName: 'on' | 'addListener'): void => {
+ it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => {
+ const listener = sinon.spy();
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName} sends the event data to the handler`, () => {
+ const listener = sinon.spy();
+ const data = {};
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ const returnValue = emitter[methodName]('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ onTests('on');
+ // we support addListener for legacy reasons
+ onTests('addListener');
+ });
+
+ describe('off', () => {
+ const offTests = (methodName: 'off' | 'removeListener'): void => {
+ it(`${methodName}: removes the listener so it is no longer called`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ emitter.off('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const returnValue = emitter.off('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ offTests('off');
+ // we support removeListener for legacy reasons
+ offTests('removeListener');
+ });
+
+ describe('once', () => {
+ it('only calls the listener once and then removes it', () => {
+ const listener = sinon.spy();
+ emitter.once('foo', listener);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ emitter.emit('foo', undefined);
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it('supports chaining', () => {
+ const listener = sinon.spy();
+ const returnValue = emitter.once('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ });
+
+ describe('emit', () => {
+ it('calls all the listeners for an event', () => {
+ const listener1 = sinon.spy();
+ const listener2 = sinon.spy();
+ const listener3 = sinon.spy();
+ emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3);
+
+ emitter.emit('foo', undefined);
+
+ expect(listener1.callCount).toEqual(1);
+ expect(listener2.callCount).toEqual(1);
+ expect(listener3.callCount).toEqual(0);
+ });
+
+ it('passes data through to the listener', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const data = {};
+
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it('returns true if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('foo', undefined)).toBe(true);
+ });
+
+ it('returns false if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('notFoo', undefined)).toBe(false);
+ });
+ });
+
+ describe('listenerCount', () => {
+ it('returns the number of listeners for the given event', () => {
+ emitter.on('foo', () => {});
+ emitter.on('foo', () => {});
+ emitter.on('bar', () => {});
+ expect(emitter.listenerCount('foo')).toEqual(2);
+ expect(emitter.listenerCount('bar')).toEqual(1);
+ expect(emitter.listenerCount('noListeners')).toEqual(0);
+ });
+ });
+
+ describe('removeAllListeners', () => {
+ it('removes every listener from all events by default', () => {
+ emitter.on('foo', () => {}).on('bar', () => {});
+
+ emitter.removeAllListeners();
+ expect(emitter.emit('foo', undefined)).toBe(false);
+ expect(emitter.emit('bar', undefined)).toBe(false);
+ });
+
+ it('returns the emitter for chaining', () => {
+ expect(emitter.removeAllListeners()).toBe(emitter);
+ });
+
+ it('can filter to remove only listeners for a given event name', () => {
+ emitter
+ .on('foo', () => {})
+ .on('bar', () => {})
+ .on('bar', () => {});
+
+ emitter.removeAllListeners('bar');
+ expect(emitter.emit('foo', undefined)).toBe(true);
+ expect(emitter.emit('bar', undefined)).toBe(false);
+ });
+ });
+
+ describe('dispose', () => {
+ it('should dispose higher order emitters properly', () => {
+ let values = '';
+ emitter.on('foo', () => {
+ values += '1';
+ });
+ const higherOrderEmitter = new EventEmitter(emitter);
+
+ higherOrderEmitter.on('foo', () => {
+ values += '2';
+ });
+ higherOrderEmitter.emit('foo', undefined);
+
+ expect(values).toMatch('12');
+
+ higherOrderEmitter.off('foo');
+ higherOrderEmitter.emit('foo', undefined);
+
+ expect(values).toMatch('121');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
new file mode 100644
index 0000000000..4a8bcb801f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts
@@ -0,0 +1,253 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import mitt, {type Emitter} from '../../third_party/mitt/mitt.js';
+import {disposeSymbol} from '../util/disposable.js';
+
+/**
+ * @public
+ */
+export type EventType = string | symbol;
+
+/**
+ * @public
+ */
+export type Handler<T = unknown> = (event: T) => void;
+
+/**
+ * @public
+ */
+export interface CommonEventEmitter<Events extends Record<EventType, unknown>> {
+ on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): this;
+ off<Key extends keyof Events>(
+ type: Key,
+ handler?: Handler<Events[Key]>
+ ): this;
+ emit<Key extends keyof Events>(type: Key, event: Events[Key]): boolean;
+ /* To maintain parity with the built in NodeJS event emitter which uses removeListener
+ * rather than `off`.
+ * If you're implementing new code you should use `off`.
+ */
+ addListener<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ removeListener<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ once<Key extends keyof Events>(
+ type: Key,
+ handler: Handler<Events[Key]>
+ ): this;
+ listenerCount(event: keyof Events): number;
+
+ removeAllListeners(event?: keyof Events): this;
+}
+
+/**
+ * @public
+ */
+export type EventsWithWildcard<Events extends Record<EventType, unknown>> =
+ Events & {
+ '*': Events[keyof Events];
+ };
+
+/**
+ * The EventEmitter class that many Puppeteer classes extend.
+ *
+ * @remarks
+ *
+ * This allows you to listen to events that Puppeteer classes fire and act
+ * accordingly. Therefore you'll mostly use {@link EventEmitter.on | on} and
+ * {@link EventEmitter.off | off} to bind
+ * and unbind to event listeners.
+ *
+ * @public
+ */
+export class EventEmitter<Events extends Record<EventType, unknown>>
+ implements CommonEventEmitter<EventsWithWildcard<Events>>
+{
+ #emitter: Emitter<EventsWithWildcard<Events>> | EventEmitter<Events>;
+ #handlers = new Map<keyof Events | '*', Array<Handler<any>>>();
+
+ /**
+ * If you pass an emitter, the returned emitter will wrap the passed emitter.
+ *
+ * @internal
+ */
+ constructor(
+ emitter: Emitter<EventsWithWildcard<Events>> | EventEmitter<Events> = mitt(
+ new Map()
+ )
+ ) {
+ this.#emitter = emitter;
+ }
+
+ /**
+ * Bind an event listener to fire when an event occurs.
+ * @param type - the event type you'd like to listen to. Can be a string or symbol.
+ * @param handler - the function to be called when the event occurs.
+ * @returns `this` to enable you to chain method calls.
+ */
+ on<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const handlers = this.#handlers.get(type);
+ if (handlers === undefined) {
+ this.#handlers.set(type, [handler]);
+ } else {
+ handlers.push(handler);
+ }
+
+ this.#emitter.on(type, handler);
+ return this;
+ }
+
+ /**
+ * Remove an event listener from firing.
+ * @param type - the event type you'd like to stop listening to.
+ * @param handler - the function that should be removed.
+ * @returns `this` to enable you to chain method calls.
+ */
+ off<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler?: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const handlers = this.#handlers.get(type) ?? [];
+ if (handler === undefined) {
+ for (const handler of handlers) {
+ this.#emitter.off(type, handler);
+ }
+ this.#handlers.delete(type);
+ return this;
+ }
+ const index = handlers.lastIndexOf(handler);
+ if (index > -1) {
+ this.#emitter.off(type, ...handlers.splice(index, 1));
+ }
+ return this;
+ }
+
+ /**
+ * Emit an event and call any associated listeners.
+ *
+ * @param type - the event you'd like to emit
+ * @param eventData - any data you'd like to emit with the event
+ * @returns `true` if there are any listeners, `false` if there are not.
+ */
+ emit<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ event: EventsWithWildcard<Events>[Key]
+ ): boolean {
+ this.#emitter.emit(type, event);
+ return this.listenerCount(type) > 0;
+ }
+
+ /**
+ * Remove an event listener.
+ *
+ * @deprecated please use {@link EventEmitter.off} instead.
+ */
+ removeListener<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ return this.off(type, handler);
+ }
+
+ /**
+ * Add an event listener.
+ *
+ * @deprecated please use {@link EventEmitter.on} instead.
+ */
+ addListener<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ return this.on(type, handler);
+ }
+
+ /**
+ * Like `on` but the listener will only be fired once and then it will be removed.
+ * @param type - the event you'd like to listen to
+ * @param handler - the handler function to run when the event occurs
+ * @returns `this` to enable you to chain method calls.
+ */
+ once<Key extends keyof EventsWithWildcard<Events>>(
+ type: Key,
+ handler: Handler<EventsWithWildcard<Events>[Key]>
+ ): this {
+ const onceHandler: Handler<EventsWithWildcard<Events>[Key]> = eventData => {
+ handler(eventData);
+ this.off(type, onceHandler);
+ };
+
+ return this.on(type, onceHandler);
+ }
+
+ /**
+ * Gets the number of listeners for a given event.
+ *
+ * @param type - the event to get the listener count for
+ * @returns the number of listeners bound to the given event
+ */
+ listenerCount(type: keyof EventsWithWildcard<Events>): number {
+ return this.#handlers.get(type)?.length || 0;
+ }
+
+ /**
+ * Removes all listeners. If given an event argument, it will remove only
+ * listeners for that event.
+ *
+ * @param type - the event to remove listeners for.
+ * @returns `this` to enable you to chain method calls.
+ */
+ removeAllListeners(type?: keyof EventsWithWildcard<Events>): this {
+ if (type !== undefined) {
+ return this.off(type);
+ }
+ this[disposeSymbol]();
+ return this;
+ }
+
+ /**
+ * @internal
+ */
+ [disposeSymbol](): void {
+ for (const [type, handlers] of this.#handlers) {
+ for (const handler of handlers) {
+ this.#emitter.off(type, handler);
+ }
+ }
+ this.#handlers.clear();
+ }
+}
+
+/**
+ * @internal
+ */
+export class EventSubscription<
+ Target extends CommonEventEmitter<Record<Type, Event>>,
+ Type extends EventType = EventType,
+ Event = unknown,
+> {
+ #target: Target;
+ #type: Type;
+ #handler: Handler<Event>;
+
+ constructor(target: Target, type: Type, handler: Handler<Event>) {
+ this.#target = target;
+ this.#type = type;
+ this.#handler = handler;
+ this.#target.on(this.#type, this.#handler);
+ }
+
+ [disposeSymbol](): void {
+ this.#target.off(this.#type, this.#handler);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts
new file mode 100644
index 0000000000..2e4fd14fa7
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {assert} from '../util/assert.js';
+
+/**
+ * File choosers let you react to the page requesting for a file.
+ *
+ * @remarks
+ * `FileChooser` instances are returned via the {@link Page.waitForFileChooser} method.
+ *
+ * In browsers, only one file chooser can be opened at a time.
+ * All file choosers must be accepted or canceled. Not doing so will prevent
+ * subsequent file choosers from appearing.
+ *
+ * @example
+ *
+ * ```ts
+ * const [fileChooser] = await Promise.all([
+ * page.waitForFileChooser(),
+ * page.click('#upload-file-button'), // some button that triggers file selection
+ * ]);
+ * await fileChooser.accept(['/tmp/myfile.pdf']);
+ * ```
+ *
+ * @public
+ */
+export class FileChooser {
+ #element: ElementHandle<HTMLInputElement>;
+ #multiple: boolean;
+ #handled = false;
+
+ /**
+ * @internal
+ */
+ constructor(
+ element: ElementHandle<HTMLInputElement>,
+ event: Protocol.Page.FileChooserOpenedEvent
+ ) {
+ this.#element = element;
+ this.#multiple = event.mode !== 'selectSingle';
+ }
+
+ /**
+ * Whether file chooser allow for
+ * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple | multiple}
+ * file selection.
+ */
+ isMultiple(): boolean {
+ return this.#multiple;
+ }
+
+ /**
+ * Accept the file chooser request with the given file paths.
+ *
+ * @remarks This will not validate whether the file paths exists. Also, if a
+ * path is relative, then it is resolved against the
+ * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
+ * For locals script connecting to remote chrome environments, paths must be
+ * absolute.
+ */
+ async accept(paths: string[]): Promise<void> {
+ assert(
+ !this.#handled,
+ 'Cannot accept FileChooser which is already handled!'
+ );
+ this.#handled = true;
+ await this.#element.uploadFile(...paths);
+ }
+
+ /**
+ * Closes the file chooser without selecting any files.
+ */
+ async cancel(): Promise<void> {
+ assert(
+ !this.#handled,
+ 'Cannot cancel FileChooser which is already handled!'
+ );
+ this.#handled = true;
+ // XXX: These events should converted to trusted events. Perhaps do this
+ // in `DOM.setFileInputFiles`?
+ await this.#element.evaluate(element => {
+ element.dispatchEvent(new Event('cancel', {bubbles: true}));
+ });
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts
new file mode 100644
index 0000000000..1d8bb01414
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
+
+import {customQueryHandlers} from './CustomQueryHandler.js';
+import {PierceQueryHandler} from './PierceQueryHandler.js';
+import {PQueryHandler} from './PQueryHandler.js';
+import type {QueryHandler} from './QueryHandler.js';
+import {TextQueryHandler} from './TextQueryHandler.js';
+import {XPathQueryHandler} from './XPathQueryHandler.js';
+
+const BUILTIN_QUERY_HANDLERS = {
+ aria: ARIAQueryHandler,
+ pierce: PierceQueryHandler,
+ xpath: XPathQueryHandler,
+ text: TextQueryHandler,
+} as const;
+
+const QUERY_SEPARATORS = ['=', '/'];
+
+/**
+ * @internal
+ */
+export function getQueryHandlerAndSelector(selector: string): {
+ updatedSelector: string;
+ QueryHandler: typeof QueryHandler;
+} {
+ for (const handlerMap of [
+ customQueryHandlers.names().map(name => {
+ return [name, customQueryHandlers.get(name)!] as const;
+ }),
+ Object.entries(BUILTIN_QUERY_HANDLERS),
+ ]) {
+ for (const [name, QueryHandler] of handlerMap) {
+ for (const separator of QUERY_SEPARATORS) {
+ const prefix = `${name}${separator}`;
+ if (selector.startsWith(prefix)) {
+ selector = selector.slice(prefix.length);
+ return {updatedSelector: selector, QueryHandler};
+ }
+ }
+ }
+ }
+ return {updatedSelector: selector, QueryHandler: PQueryHandler};
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts
new file mode 100644
index 0000000000..c88003ed71
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+import {DisposableStack, disposeSymbol} from '../util/disposable.js';
+
+import type {AwaitableIterable, HandleFor} from './types.js';
+
+const DEFAULT_BATCH_SIZE = 20;
+
+/**
+ * This will transpose an iterator JSHandle into a fast, Puppeteer-side iterator
+ * of JSHandles.
+ *
+ * @param size - The number of elements to transpose. This should be something
+ * reasonable.
+ */
+async function* fastTransposeIteratorHandle<T>(
+ iterator: JSHandle<AwaitableIterator<T>>,
+ size: number
+) {
+ using array = await iterator.evaluateHandle(async (iterator, size) => {
+ const results = [];
+ while (results.length < size) {
+ const result = await iterator.next();
+ if (result.done) {
+ break;
+ }
+ results.push(result.value);
+ }
+ return results;
+ }, size);
+ const properties = (await array.getProperties()) as Map<string, HandleFor<T>>;
+ const handles = properties.values();
+ using stack = new DisposableStack();
+ stack.defer(() => {
+ for (using handle of handles) {
+ handle[disposeSymbol]();
+ }
+ });
+ yield* handles;
+ return properties.size === 0;
+}
+
+/**
+ * This will transpose an iterator JSHandle in batches based on the default size
+ * of {@link fastTransposeIteratorHandle}.
+ */
+
+async function* transposeIteratorHandle<T>(
+ iterator: JSHandle<AwaitableIterator<T>>
+) {
+ let size = DEFAULT_BATCH_SIZE;
+ while (!(yield* fastTransposeIteratorHandle(iterator, size))) {
+ size <<= 1;
+ }
+}
+
+type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>;
+
+/**
+ * @internal
+ */
+export async function* transposeIterableHandle<T>(
+ handle: JSHandle<AwaitableIterable<T>>
+): AsyncIterableIterator<HandleFor<T>> {
+ using generatorHandle = await handle.evaluateHandle(iterable => {
+ return (async function* () {
+ yield* iterable;
+ })();
+ });
+ yield* transposeIteratorHandle(generatorHandle);
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts
new file mode 100644
index 0000000000..ed30281dd8
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {JSHandle} from '../api/JSHandle.js';
+import type PuppeteerUtil from '../injected/injected.js';
+
+/**
+ * @internal
+ */
+export interface PuppeteerUtilWrapper {
+ puppeteerUtil: Promise<JSHandle<PuppeteerUtil>>;
+}
+
+/**
+ * @internal
+ */
+export class LazyArg<T, Context = PuppeteerUtilWrapper> {
+ static create = <T>(
+ get: (context: PuppeteerUtilWrapper) => Promise<T> | T
+ ): T => {
+ // We don't want to introduce LazyArg to the type system, otherwise we would
+ // have to make it public.
+ return new LazyArg(get) as unknown as T;
+ };
+
+ #get: (context: Context) => Promise<T> | T;
+ private constructor(get: (context: Context) => Promise<T> | T) {
+ this.#get = get;
+ }
+
+ async get(context: Context): Promise<T> {
+ return await this.#get(context);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts
new file mode 100644
index 0000000000..eae26252d1
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManagerEvents.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {HTTPRequest} from '../api/HTTPRequest.js';
+import type {HTTPResponse} from '../api/HTTPResponse.js';
+
+import type {EventType} from './EventEmitter.js';
+
+/**
+ * We use symbols to prevent any external parties listening to these events.
+ * They are internal to Puppeteer.
+ *
+ * @internal
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace NetworkManagerEvent {
+ export const Request = Symbol('NetworkManager.Request');
+ export const RequestServedFromCache = Symbol(
+ 'NetworkManager.RequestServedFromCache'
+ );
+ export const Response = Symbol('NetworkManager.Response');
+ export const RequestFailed = Symbol('NetworkManager.RequestFailed');
+ export const RequestFinished = Symbol('NetworkManager.RequestFinished');
+}
+
+/**
+ * @internal
+ */
+export interface NetworkManagerEvents extends Record<EventType, unknown> {
+ [NetworkManagerEvent.Request]: HTTPRequest;
+ [NetworkManagerEvent.RequestServedFromCache]: HTTPRequest | undefined;
+ [NetworkManagerEvent.Response]: HTTPResponse;
+ [NetworkManagerEvent.RequestFailed]: HTTPRequest;
+ [NetworkManagerEvent.RequestFinished]: HTTPRequest;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
new file mode 100644
index 0000000000..7cae9191a9
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts
@@ -0,0 +1,217 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface PDFMargin {
+ top?: string | number;
+ bottom?: string | number;
+ left?: string | number;
+ right?: string | number;
+}
+
+/**
+ * @public
+ */
+export type LowerCasePaperFormat =
+ | 'letter'
+ | 'legal'
+ | 'tabloid'
+ | 'ledger'
+ | 'a0'
+ | 'a1'
+ | 'a2'
+ | 'a3'
+ | 'a4'
+ | 'a5'
+ | 'a6';
+
+/**
+ * All the valid paper format types when printing a PDF.
+ *
+ * @remarks
+ *
+ * The sizes of each format are as follows:
+ *
+ * - `Letter`: 8.5in x 11in
+ *
+ * - `Legal`: 8.5in x 14in
+ *
+ * - `Tabloid`: 11in x 17in
+ *
+ * - `Ledger`: 17in x 11in
+ *
+ * - `A0`: 33.1in x 46.8in
+ *
+ * - `A1`: 23.4in x 33.1in
+ *
+ * - `A2`: 16.54in x 23.4in
+ *
+ * - `A3`: 11.7in x 16.54in
+ *
+ * - `A4`: 8.27in x 11.7in
+ *
+ * - `A5`: 5.83in x 8.27in
+ *
+ * - `A6`: 4.13in x 5.83in
+ *
+ * @public
+ */
+export type PaperFormat =
+ | Uppercase<LowerCasePaperFormat>
+ | Capitalize<LowerCasePaperFormat>
+ | LowerCasePaperFormat;
+
+/**
+ * Valid options to configure PDF generation via {@link Page.pdf}.
+ * @public
+ */
+export interface PDFOptions {
+ /**
+ * Scales the rendering of the web page. Amount must be between `0.1` and `2`.
+ * @defaultValue `1`
+ */
+ scale?: number;
+ /**
+ * Whether to show the header and footer.
+ * @defaultValue `false`
+ */
+ displayHeaderFooter?: boolean;
+ /**
+ * HTML template for the print header. Should be valid HTML with the following
+ * classes used to inject values into them:
+ *
+ * - `date` formatted print date
+ *
+ * - `title` document title
+ *
+ * - `url` document location
+ *
+ * - `pageNumber` current page number
+ *
+ * - `totalPages` total pages in the document
+ */
+ headerTemplate?: string;
+ /**
+ * HTML template for the print footer. Has the same constraints and support
+ * for special classes as {@link PDFOptions | PDFOptions.headerTemplate}.
+ */
+ footerTemplate?: string;
+ /**
+ * Set to `true` to print background graphics.
+ * @defaultValue `false`
+ */
+ printBackground?: boolean;
+ /**
+ * Whether to print in landscape orientation.
+ * @defaultValue `false`
+ */
+ landscape?: boolean;
+ /**
+ * Paper ranges to print, e.g. `1-5, 8, 11-13`.
+ * @defaultValue The empty string, which means all pages are printed.
+ */
+ pageRanges?: string;
+ /**
+ * @remarks
+ * If set, this takes priority over the `width` and `height` options.
+ * @defaultValue `letter`.
+ */
+ format?: PaperFormat;
+ /**
+ * Sets the width of paper. You can pass in a number or a string with a unit.
+ */
+ width?: string | number;
+ /**
+ * Sets the height of paper. You can pass in a number or a string with a unit.
+ */
+ height?: string | number;
+ /**
+ * Give any CSS `@page` size declared in the page priority over what is
+ * declared in the `width` or `height` or `format` option.
+ * @defaultValue `false`, which will scale the content to fit the paper size.
+ */
+ preferCSSPageSize?: boolean;
+ /**
+ * Set the PDF margins.
+ * @defaultValue `undefined` no margins are set.
+ */
+ margin?: PDFMargin;
+ /**
+ * The path to save the file to.
+ *
+ * @remarks
+ *
+ * If the path is relative, it's resolved relative to the current working directory.
+ *
+ * @defaultValue `undefined`, which means the PDF will not be written to disk.
+ */
+ path?: string;
+ /**
+ * Hides default white background and allows generating pdfs with transparency.
+ * @defaultValue `false`
+ */
+ omitBackground?: boolean;
+ /**
+ * Generate tagged (accessible) PDF.
+ * @defaultValue `false`
+ * @experimental
+ */
+ tagged?: boolean;
+ /**
+ * Timeout in milliseconds. Pass `0` to disable timeout.
+ * @defaultValue `30_000`
+ */
+ timeout?: number;
+}
+
+/**
+ * @internal
+ */
+export interface PaperFormatDimensions {
+ width: number;
+ height: number;
+}
+
+/**
+ * @internal
+ */
+export interface ParsedPDFOptionsInterface {
+ width: number;
+ height: number;
+ margin: {
+ top: number;
+ bottom: number;
+ left: number;
+ right: number;
+ };
+}
+
+/**
+ * @internal
+ */
+export type ParsedPDFOptions = Required<
+ Omit<PDFOptions, 'path' | 'format' | 'timeout'> & ParsedPDFOptionsInterface
+>;
+
+/**
+ * @internal
+ */
+export const paperFormats: Record<LowerCasePaperFormat, PaperFormatDimensions> =
+ {
+ letter: {width: 8.5, height: 11},
+ legal: {width: 8.5, height: 14},
+ tabloid: {width: 11, height: 17},
+ ledger: {width: 17, height: 11},
+ a0: {width: 33.1, height: 46.8},
+ a1: {width: 23.4, height: 33.1},
+ a2: {width: 16.54, height: 23.4},
+ a3: {width: 11.7, height: 16.54},
+ a4: {width: 8.27, height: 11.7},
+ a5: {width: 5.83, height: 8.27},
+ a6: {width: 4.13, height: 5.83},
+ } as const;
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts
new file mode 100644
index 0000000000..db9b832d77
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ QueryHandler,
+ type QuerySelector,
+ type QuerySelectorAll,
+} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class PQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {pQuerySelectorAll}
+ ) => {
+ return pQuerySelectorAll(element, selector);
+ };
+ static override querySelector: QuerySelector = (
+ element,
+ selector,
+ {pQuerySelector}
+ ) => {
+ return pQuerySelector(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts
new file mode 100644
index 0000000000..36ddbe7f3e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type PuppeteerUtil from '../injected/injected.js';
+
+import {QueryHandler} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class PierceQueryHandler extends QueryHandler {
+ static override querySelector = (
+ element: Node,
+ selector: string,
+ {pierceQuerySelector}: PuppeteerUtil
+ ): Node | null => {
+ return pierceQuerySelector(element, selector);
+ };
+ static override querySelectorAll = (
+ element: Node,
+ selector: string,
+ {pierceQuerySelectorAll}: PuppeteerUtil
+ ): Iterable<Node> => {
+ return pierceQuerySelectorAll(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts
new file mode 100644
index 0000000000..dcd75aceb6
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Supported products.
+ * @public
+ */
+export type Product = 'chrome' | 'firefox';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts
new file mode 100644
index 0000000000..844a3622bd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Browser} from '../api/Browser.js';
+
+import {_connectToBrowser} from './BrowserConnector.js';
+import type {ConnectOptions} from './ConnectOptions.js';
+import {
+ type CustomQueryHandler,
+ customQueryHandlers,
+} from './CustomQueryHandler.js';
+
+/**
+ * Settings that are common to the Puppeteer class, regardless of environment.
+ *
+ * @internal
+ */
+export interface CommonPuppeteerSettings {
+ isPuppeteerCore: boolean;
+}
+
+/**
+ * The main Puppeteer class.
+ *
+ * IMPORTANT: if you are using Puppeteer in a Node environment, you will get an
+ * instance of {@link PuppeteerNode} when you import or require `puppeteer`.
+ * That class extends `Puppeteer`, so has all the methods documented below as
+ * well as all that are defined on {@link PuppeteerNode}.
+ *
+ * @public
+ */
+export class Puppeteer {
+ /**
+ * Operations for {@link CustomQueryHandler | custom query handlers}. See
+ * {@link CustomQueryHandlerRegistry}.
+ *
+ * @internal
+ */
+ static customQueryHandlers = customQueryHandlers;
+
+ /**
+ * Registers a {@link CustomQueryHandler | custom query handler}.
+ *
+ * @remarks
+ * After registration, the handler can be used everywhere where a selector is
+ * expected by prepending the selection string with `<name>/`. The name is only
+ * allowed to consist of lower- and upper case latin letters.
+ *
+ * @example
+ *
+ * ```
+ * puppeteer.registerCustomQueryHandler('text', { … });
+ * const aHandle = await page.$('text/…');
+ * ```
+ *
+ * @param name - The name that the custom query handler will be registered
+ * under.
+ * @param queryHandler - The {@link CustomQueryHandler | custom query handler}
+ * to register.
+ *
+ * @public
+ */
+ static registerCustomQueryHandler(
+ name: string,
+ queryHandler: CustomQueryHandler
+ ): void {
+ return this.customQueryHandlers.register(name, queryHandler);
+ }
+
+ /**
+ * Unregisters a custom query handler for a given name.
+ */
+ static unregisterCustomQueryHandler(name: string): void {
+ return this.customQueryHandlers.unregister(name);
+ }
+
+ /**
+ * Gets the names of all custom query handlers.
+ */
+ static customQueryHandlerNames(): string[] {
+ return this.customQueryHandlers.names();
+ }
+
+ /**
+ * Unregisters all custom query handlers.
+ */
+ static clearCustomQueryHandlers(): void {
+ return this.customQueryHandlers.clear();
+ }
+
+ /**
+ * @internal
+ */
+ _isPuppeteerCore: boolean;
+ /**
+ * @internal
+ */
+ protected _changedProduct = false;
+
+ /**
+ * @internal
+ */
+ constructor(settings: CommonPuppeteerSettings) {
+ this._isPuppeteerCore = settings.isPuppeteerCore;
+
+ this.connect = this.connect.bind(this);
+ }
+
+ /**
+ * This method attaches Puppeteer to an existing browser instance.
+ *
+ * @remarks
+ *
+ * @param options - Set of configurable options to set on the browser.
+ * @returns Promise which resolves to browser instance.
+ */
+ connect(options: ConnectOptions): Promise<Browser> {
+ return _connectToBrowser(options);
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts
new file mode 100644
index 0000000000..1655c7dba2
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts
@@ -0,0 +1,205 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {_isElementHandle} from '../api/ElementHandleSymbol.js';
+import type {Frame} from '../api/Frame.js';
+import type {WaitForSelectorOptions} from '../api/Page.js';
+import type PuppeteerUtil from '../injected/injected.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+import {interpolateFunction, stringifyFunction} from '../util/Function.js';
+
+import {transposeIterableHandle} from './HandleIterator.js';
+import {LazyArg} from './LazyArg.js';
+import type {Awaitable, AwaitableIterable} from './types.js';
+
+/**
+ * @internal
+ */
+export type QuerySelectorAll = (
+ node: Node,
+ selector: string,
+ PuppeteerUtil: PuppeteerUtil
+) => AwaitableIterable<Node>;
+
+/**
+ * @internal
+ */
+export type QuerySelector = (
+ node: Node,
+ selector: string,
+ PuppeteerUtil: PuppeteerUtil
+) => Awaitable<Node | null>;
+
+/**
+ * @internal
+ */
+export class QueryHandler {
+ // Either one of these may be implemented, but at least one must be.
+ static querySelectorAll?: QuerySelectorAll;
+ static querySelector?: QuerySelector;
+
+ static get _querySelector(): QuerySelector {
+ if (this.querySelector) {
+ return this.querySelector;
+ }
+ if (!this.querySelectorAll) {
+ throw new Error('Cannot create default `querySelector`.');
+ }
+
+ return (this.querySelector = interpolateFunction(
+ async (node, selector, PuppeteerUtil) => {
+ const querySelectorAll: QuerySelectorAll =
+ PLACEHOLDER('querySelectorAll');
+ const results = querySelectorAll(node, selector, PuppeteerUtil);
+ for await (const result of results) {
+ return result;
+ }
+ return null;
+ },
+ {
+ querySelectorAll: stringifyFunction(this.querySelectorAll),
+ }
+ ));
+ }
+
+ static get _querySelectorAll(): QuerySelectorAll {
+ if (this.querySelectorAll) {
+ return this.querySelectorAll;
+ }
+ if (!this.querySelector) {
+ throw new Error('Cannot create default `querySelectorAll`.');
+ }
+
+ return (this.querySelectorAll = interpolateFunction(
+ async function* (node, selector, PuppeteerUtil) {
+ const querySelector: QuerySelector = PLACEHOLDER('querySelector');
+ const result = await querySelector(node, selector, PuppeteerUtil);
+ if (result) {
+ yield result;
+ }
+ },
+ {
+ querySelector: stringifyFunction(this.querySelector),
+ }
+ ));
+ }
+
+ /**
+ * Queries for multiple nodes given a selector and {@link ElementHandle}.
+ *
+ * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll | Document.querySelectorAll()}.
+ */
+ static async *queryAll(
+ element: ElementHandle<Node>,
+ selector: string
+ ): AwaitableIterable<ElementHandle<Node>> {
+ using handle = await element.evaluateHandle(
+ this._querySelectorAll,
+ selector,
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ yield* transposeIterableHandle(handle);
+ }
+
+ /**
+ * Queries for a single node given a selector and {@link ElementHandle}.
+ *
+ * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector}.
+ */
+ static async queryOne(
+ element: ElementHandle<Node>,
+ selector: string
+ ): Promise<ElementHandle<Node> | null> {
+ using result = await element.evaluateHandle(
+ this._querySelector,
+ selector,
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ if (!(_isElementHandle in result)) {
+ return null;
+ }
+ return result.move();
+ }
+
+ /**
+ * Waits until a single node appears for a given selector and
+ * {@link ElementHandle}.
+ *
+ * This will always query the handle in the Puppeteer world and migrate the
+ * result to the main world.
+ */
+ static async waitFor(
+ elementOrFrame: ElementHandle<Node> | Frame,
+ selector: string,
+ options: WaitForSelectorOptions
+ ): Promise<ElementHandle<Node> | null> {
+ let frame!: Frame;
+ using element = await (async () => {
+ if (!(_isElementHandle in elementOrFrame)) {
+ frame = elementOrFrame;
+ return;
+ }
+ frame = elementOrFrame.frame;
+ return await frame.isolatedRealm().adoptHandle(elementOrFrame);
+ })();
+
+ const {visible = false, hidden = false, timeout, signal} = options;
+
+ try {
+ signal?.throwIfAborted();
+
+ using handle = await frame.isolatedRealm().waitForFunction(
+ async (PuppeteerUtil, query, selector, root, visible) => {
+ const querySelector = PuppeteerUtil.createFunction(
+ query
+ ) as QuerySelector;
+ const node = await querySelector(
+ root ?? document,
+ selector,
+ PuppeteerUtil
+ );
+ return PuppeteerUtil.checkVisibility(node, visible);
+ },
+ {
+ polling: visible || hidden ? 'raf' : 'mutation',
+ root: element,
+ timeout,
+ signal,
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ stringifyFunction(this._querySelector),
+ selector,
+ element,
+ visible ? true : hidden ? false : undefined
+ );
+
+ if (signal?.aborted) {
+ throw signal.reason;
+ }
+
+ if (!(_isElementHandle in handle)) {
+ return null;
+ }
+ return await frame.mainRealm().transferHandle(handle);
+ } catch (error) {
+ if (!isErrorLike(error)) {
+ throw error;
+ }
+ if (error.name === 'AbortError') {
+ throw error;
+ }
+ error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
+ throw error;
+ }
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
new file mode 100644
index 0000000000..0264c9175f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts
@@ -0,0 +1,52 @@
+import {source as injectedSource} from '../generated/injected.js';
+
+/**
+ * @internal
+ */
+export class ScriptInjector {
+ #updated = false;
+ #amendments = new Set<string>();
+
+ // Appends a statement of the form `(PuppeteerUtil) => {...}`.
+ append(statement: string): void {
+ this.#update(() => {
+ this.#amendments.add(statement);
+ });
+ }
+
+ pop(statement: string): void {
+ this.#update(() => {
+ this.#amendments.delete(statement);
+ });
+ }
+
+ inject(inject: (script: string) => void, force = false): void {
+ if (this.#updated || force) {
+ inject(this.#get());
+ }
+ this.#updated = false;
+ }
+
+ #update(callback: () => void): void {
+ callback();
+ this.#updated = true;
+ }
+
+ #get(): string {
+ return `(() => {
+ const module = {};
+ ${injectedSource}
+ ${[...this.#amendments]
+ .map(statement => {
+ return `(${statement})(module.exports.default);`;
+ })
+ .join('')}
+ return module.exports.default;
+ })()`;
+ }
+}
+
+/**
+ * @internal
+ */
+export const scriptInjector = new ScriptInjector();
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts
new file mode 100644
index 0000000000..188eeea9ad
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts
@@ -0,0 +1,78 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {Protocol} from 'devtools-protocol';
+
+/**
+ * The SecurityDetails class represents the security details of a
+ * response that was received over a secure connection.
+ *
+ * @public
+ */
+export class SecurityDetails {
+ #subjectName: string;
+ #issuer: string;
+ #validFrom: number;
+ #validTo: number;
+ #protocol: string;
+ #sanList: string[];
+
+ /**
+ * @internal
+ */
+ constructor(securityPayload: Protocol.Network.SecurityDetails) {
+ this.#subjectName = securityPayload.subjectName;
+ this.#issuer = securityPayload.issuer;
+ this.#validFrom = securityPayload.validFrom;
+ this.#validTo = securityPayload.validTo;
+ this.#protocol = securityPayload.protocol;
+ this.#sanList = securityPayload.sanList;
+ }
+
+ /**
+ * The name of the issuer of the certificate.
+ */
+ issuer(): string {
+ return this.#issuer;
+ }
+
+ /**
+ * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp}
+ * marking the start of the certificate's validity.
+ */
+ validFrom(): number {
+ return this.#validFrom;
+ }
+
+ /**
+ * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp}
+ * marking the end of the certificate's validity.
+ */
+ validTo(): number {
+ return this.#validTo;
+ }
+
+ /**
+ * The security protocol being used, e.g. "TLS 1.2".
+ */
+ protocol(): string {
+ return this.#protocol;
+ }
+
+ /**
+ * The name of the subject to which the certificate was issued.
+ */
+ subjectName(): string {
+ return this.#subjectName;
+ }
+
+ /**
+ * The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate.
+ */
+ subjectAlternativeNames(): string[] {
+ return this.#sanList;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts
new file mode 100644
index 0000000000..3ad1409c1b
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export class TaskQueue {
+ #chain: Promise<void>;
+
+ constructor() {
+ this.#chain = Promise.resolve();
+ }
+
+ postTask<T>(task: () => Promise<T>): Promise<T> {
+ const result = this.#chain.then(task);
+ this.#chain = result.then(
+ () => {
+ return undefined;
+ },
+ () => {
+ return undefined;
+ }
+ );
+ return result;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts
new file mode 100644
index 0000000000..450ed06957
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {QueryHandler, type QuerySelectorAll} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class TextQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {textQuerySelectorAll}
+ ) => {
+ return textQuerySelectorAll(element, selector);
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts
new file mode 100644
index 0000000000..7789d89b75
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const DEFAULT_TIMEOUT = 30000;
+
+/**
+ * @internal
+ */
+export class TimeoutSettings {
+ #defaultTimeout: number | null;
+ #defaultNavigationTimeout: number | null;
+
+ constructor() {
+ this.#defaultTimeout = null;
+ this.#defaultNavigationTimeout = null;
+ }
+
+ setDefaultTimeout(timeout: number): void {
+ this.#defaultTimeout = timeout;
+ }
+
+ setDefaultNavigationTimeout(timeout: number): void {
+ this.#defaultNavigationTimeout = timeout;
+ }
+
+ navigationTimeout(): number {
+ if (this.#defaultNavigationTimeout !== null) {
+ return this.#defaultNavigationTimeout;
+ }
+ if (this.#defaultTimeout !== null) {
+ return this.#defaultTimeout;
+ }
+ return DEFAULT_TIMEOUT;
+ }
+
+ timeout(): number {
+ if (this.#defaultTimeout !== null) {
+ return this.#defaultTimeout;
+ }
+ return DEFAULT_TIMEOUT;
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts
new file mode 100644
index 0000000000..0a6d2f2e18
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts
@@ -0,0 +1,671 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @internal
+ */
+export interface KeyDefinition {
+ keyCode?: number;
+ shiftKeyCode?: number;
+ key?: string;
+ shiftKey?: string;
+ code?: string;
+ text?: string;
+ shiftText?: string;
+ location?: number;
+}
+
+/**
+ * All the valid keys that can be passed to functions that take user input, such
+ * as {@link Keyboard.press | keyboard.press }
+ *
+ * @public
+ */
+export type KeyInput =
+ | '0'
+ | '1'
+ | '2'
+ | '3'
+ | '4'
+ | '5'
+ | '6'
+ | '7'
+ | '8'
+ | '9'
+ | 'Power'
+ | 'Eject'
+ | 'Abort'
+ | 'Help'
+ | 'Backspace'
+ | 'Tab'
+ | 'Numpad5'
+ | 'NumpadEnter'
+ | 'Enter'
+ | '\r'
+ | '\n'
+ | 'ShiftLeft'
+ | 'ShiftRight'
+ | 'ControlLeft'
+ | 'ControlRight'
+ | 'AltLeft'
+ | 'AltRight'
+ | 'Pause'
+ | 'CapsLock'
+ | 'Escape'
+ | 'Convert'
+ | 'NonConvert'
+ | 'Space'
+ | 'Numpad9'
+ | 'PageUp'
+ | 'Numpad3'
+ | 'PageDown'
+ | 'End'
+ | 'Numpad1'
+ | 'Home'
+ | 'Numpad7'
+ | 'ArrowLeft'
+ | 'Numpad4'
+ | 'Numpad8'
+ | 'ArrowUp'
+ | 'ArrowRight'
+ | 'Numpad6'
+ | 'Numpad2'
+ | 'ArrowDown'
+ | 'Select'
+ | 'Open'
+ | 'PrintScreen'
+ | 'Insert'
+ | 'Numpad0'
+ | 'Delete'
+ | 'NumpadDecimal'
+ | 'Digit0'
+ | 'Digit1'
+ | 'Digit2'
+ | 'Digit3'
+ | 'Digit4'
+ | 'Digit5'
+ | 'Digit6'
+ | 'Digit7'
+ | 'Digit8'
+ | 'Digit9'
+ | 'KeyA'
+ | 'KeyB'
+ | 'KeyC'
+ | 'KeyD'
+ | 'KeyE'
+ | 'KeyF'
+ | 'KeyG'
+ | 'KeyH'
+ | 'KeyI'
+ | 'KeyJ'
+ | 'KeyK'
+ | 'KeyL'
+ | 'KeyM'
+ | 'KeyN'
+ | 'KeyO'
+ | 'KeyP'
+ | 'KeyQ'
+ | 'KeyR'
+ | 'KeyS'
+ | 'KeyT'
+ | 'KeyU'
+ | 'KeyV'
+ | 'KeyW'
+ | 'KeyX'
+ | 'KeyY'
+ | 'KeyZ'
+ | 'MetaLeft'
+ | 'MetaRight'
+ | 'ContextMenu'
+ | 'NumpadMultiply'
+ | 'NumpadAdd'
+ | 'NumpadSubtract'
+ | 'NumpadDivide'
+ | 'F1'
+ | 'F2'
+ | 'F3'
+ | 'F4'
+ | 'F5'
+ | 'F6'
+ | 'F7'
+ | 'F8'
+ | 'F9'
+ | 'F10'
+ | 'F11'
+ | 'F12'
+ | 'F13'
+ | 'F14'
+ | 'F15'
+ | 'F16'
+ | 'F17'
+ | 'F18'
+ | 'F19'
+ | 'F20'
+ | 'F21'
+ | 'F22'
+ | 'F23'
+ | 'F24'
+ | 'NumLock'
+ | 'ScrollLock'
+ | 'AudioVolumeMute'
+ | 'AudioVolumeDown'
+ | 'AudioVolumeUp'
+ | 'MediaTrackNext'
+ | 'MediaTrackPrevious'
+ | 'MediaStop'
+ | 'MediaPlayPause'
+ | 'Semicolon'
+ | 'Equal'
+ | 'NumpadEqual'
+ | 'Comma'
+ | 'Minus'
+ | 'Period'
+ | 'Slash'
+ | 'Backquote'
+ | 'BracketLeft'
+ | 'Backslash'
+ | 'BracketRight'
+ | 'Quote'
+ | 'AltGraph'
+ | 'Props'
+ | 'Cancel'
+ | 'Clear'
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Accept'
+ | 'ModeChange'
+ | ' '
+ | 'Print'
+ | 'Execute'
+ | '\u0000'
+ | 'a'
+ | 'b'
+ | 'c'
+ | 'd'
+ | 'e'
+ | 'f'
+ | 'g'
+ | 'h'
+ | 'i'
+ | 'j'
+ | 'k'
+ | 'l'
+ | 'm'
+ | 'n'
+ | 'o'
+ | 'p'
+ | 'q'
+ | 'r'
+ | 's'
+ | 't'
+ | 'u'
+ | 'v'
+ | 'w'
+ | 'x'
+ | 'y'
+ | 'z'
+ | 'Meta'
+ | '*'
+ | '+'
+ | '-'
+ | '/'
+ | ';'
+ | '='
+ | ','
+ | '.'
+ | '`'
+ | '['
+ | '\\'
+ | ']'
+ | "'"
+ | 'Attn'
+ | 'CrSel'
+ | 'ExSel'
+ | 'EraseEof'
+ | 'Play'
+ | 'ZoomOut'
+ | ')'
+ | '!'
+ | '@'
+ | '#'
+ | '$'
+ | '%'
+ | '^'
+ | '&'
+ | '('
+ | 'A'
+ | 'B'
+ | 'C'
+ | 'D'
+ | 'E'
+ | 'F'
+ | 'G'
+ | 'H'
+ | 'I'
+ | 'J'
+ | 'K'
+ | 'L'
+ | 'M'
+ | 'N'
+ | 'O'
+ | 'P'
+ | 'Q'
+ | 'R'
+ | 'S'
+ | 'T'
+ | 'U'
+ | 'V'
+ | 'W'
+ | 'X'
+ | 'Y'
+ | 'Z'
+ | ':'
+ | '<'
+ | '_'
+ | '>'
+ | '?'
+ | '~'
+ | '{'
+ | '|'
+ | '}'
+ | '"'
+ | 'SoftLeft'
+ | 'SoftRight'
+ | 'Camera'
+ | 'Call'
+ | 'EndCall'
+ | 'VolumeDown'
+ | 'VolumeUp';
+
+/**
+ * @internal
+ */
+export const _keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
+ '0': {keyCode: 48, key: '0', code: 'Digit0'},
+ '1': {keyCode: 49, key: '1', code: 'Digit1'},
+ '2': {keyCode: 50, key: '2', code: 'Digit2'},
+ '3': {keyCode: 51, key: '3', code: 'Digit3'},
+ '4': {keyCode: 52, key: '4', code: 'Digit4'},
+ '5': {keyCode: 53, key: '5', code: 'Digit5'},
+ '6': {keyCode: 54, key: '6', code: 'Digit6'},
+ '7': {keyCode: 55, key: '7', code: 'Digit7'},
+ '8': {keyCode: 56, key: '8', code: 'Digit8'},
+ '9': {keyCode: 57, key: '9', code: 'Digit9'},
+ Power: {key: 'Power', code: 'Power'},
+ Eject: {key: 'Eject', code: 'Eject'},
+ Abort: {keyCode: 3, code: 'Abort', key: 'Cancel'},
+ Help: {keyCode: 6, code: 'Help', key: 'Help'},
+ Backspace: {keyCode: 8, code: 'Backspace', key: 'Backspace'},
+ Tab: {keyCode: 9, code: 'Tab', key: 'Tab'},
+ Numpad5: {
+ keyCode: 12,
+ shiftKeyCode: 101,
+ key: 'Clear',
+ code: 'Numpad5',
+ shiftKey: '5',
+ location: 3,
+ },
+ NumpadEnter: {
+ keyCode: 13,
+ code: 'NumpadEnter',
+ key: 'Enter',
+ text: '\r',
+ location: 3,
+ },
+ Enter: {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ '\r': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ '\n': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'},
+ ShiftLeft: {keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1},
+ ShiftRight: {keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2},
+ ControlLeft: {
+ keyCode: 17,
+ code: 'ControlLeft',
+ key: 'Control',
+ location: 1,
+ },
+ ControlRight: {
+ keyCode: 17,
+ code: 'ControlRight',
+ key: 'Control',
+ location: 2,
+ },
+ AltLeft: {keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1},
+ AltRight: {keyCode: 18, code: 'AltRight', key: 'Alt', location: 2},
+ Pause: {keyCode: 19, code: 'Pause', key: 'Pause'},
+ CapsLock: {keyCode: 20, code: 'CapsLock', key: 'CapsLock'},
+ Escape: {keyCode: 27, code: 'Escape', key: 'Escape'},
+ Convert: {keyCode: 28, code: 'Convert', key: 'Convert'},
+ NonConvert: {keyCode: 29, code: 'NonConvert', key: 'NonConvert'},
+ Space: {keyCode: 32, code: 'Space', key: ' '},
+ Numpad9: {
+ keyCode: 33,
+ shiftKeyCode: 105,
+ key: 'PageUp',
+ code: 'Numpad9',
+ shiftKey: '9',
+ location: 3,
+ },
+ PageUp: {keyCode: 33, code: 'PageUp', key: 'PageUp'},
+ Numpad3: {
+ keyCode: 34,
+ shiftKeyCode: 99,
+ key: 'PageDown',
+ code: 'Numpad3',
+ shiftKey: '3',
+ location: 3,
+ },
+ PageDown: {keyCode: 34, code: 'PageDown', key: 'PageDown'},
+ End: {keyCode: 35, code: 'End', key: 'End'},
+ Numpad1: {
+ keyCode: 35,
+ shiftKeyCode: 97,
+ key: 'End',
+ code: 'Numpad1',
+ shiftKey: '1',
+ location: 3,
+ },
+ Home: {keyCode: 36, code: 'Home', key: 'Home'},
+ Numpad7: {
+ keyCode: 36,
+ shiftKeyCode: 103,
+ key: 'Home',
+ code: 'Numpad7',
+ shiftKey: '7',
+ location: 3,
+ },
+ ArrowLeft: {keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft'},
+ Numpad4: {
+ keyCode: 37,
+ shiftKeyCode: 100,
+ key: 'ArrowLeft',
+ code: 'Numpad4',
+ shiftKey: '4',
+ location: 3,
+ },
+ Numpad8: {
+ keyCode: 38,
+ shiftKeyCode: 104,
+ key: 'ArrowUp',
+ code: 'Numpad8',
+ shiftKey: '8',
+ location: 3,
+ },
+ ArrowUp: {keyCode: 38, code: 'ArrowUp', key: 'ArrowUp'},
+ ArrowRight: {keyCode: 39, code: 'ArrowRight', key: 'ArrowRight'},
+ Numpad6: {
+ keyCode: 39,
+ shiftKeyCode: 102,
+ key: 'ArrowRight',
+ code: 'Numpad6',
+ shiftKey: '6',
+ location: 3,
+ },
+ Numpad2: {
+ keyCode: 40,
+ shiftKeyCode: 98,
+ key: 'ArrowDown',
+ code: 'Numpad2',
+ shiftKey: '2',
+ location: 3,
+ },
+ ArrowDown: {keyCode: 40, code: 'ArrowDown', key: 'ArrowDown'},
+ Select: {keyCode: 41, code: 'Select', key: 'Select'},
+ Open: {keyCode: 43, code: 'Open', key: 'Execute'},
+ PrintScreen: {keyCode: 44, code: 'PrintScreen', key: 'PrintScreen'},
+ Insert: {keyCode: 45, code: 'Insert', key: 'Insert'},
+ Numpad0: {
+ keyCode: 45,
+ shiftKeyCode: 96,
+ key: 'Insert',
+ code: 'Numpad0',
+ shiftKey: '0',
+ location: 3,
+ },
+ Delete: {keyCode: 46, code: 'Delete', key: 'Delete'},
+ NumpadDecimal: {
+ keyCode: 46,
+ shiftKeyCode: 110,
+ code: 'NumpadDecimal',
+ key: '\u0000',
+ shiftKey: '.',
+ location: 3,
+ },
+ Digit0: {keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0'},
+ Digit1: {keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1'},
+ Digit2: {keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2'},
+ Digit3: {keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3'},
+ Digit4: {keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4'},
+ Digit5: {keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5'},
+ Digit6: {keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6'},
+ Digit7: {keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7'},
+ Digit8: {keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8'},
+ Digit9: {keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9'},
+ KeyA: {keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a'},
+ KeyB: {keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b'},
+ KeyC: {keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c'},
+ KeyD: {keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd'},
+ KeyE: {keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e'},
+ KeyF: {keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f'},
+ KeyG: {keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g'},
+ KeyH: {keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h'},
+ KeyI: {keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i'},
+ KeyJ: {keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j'},
+ KeyK: {keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k'},
+ KeyL: {keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l'},
+ KeyM: {keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm'},
+ KeyN: {keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n'},
+ KeyO: {keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o'},
+ KeyP: {keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p'},
+ KeyQ: {keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q'},
+ KeyR: {keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r'},
+ KeyS: {keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's'},
+ KeyT: {keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't'},
+ KeyU: {keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u'},
+ KeyV: {keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v'},
+ KeyW: {keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w'},
+ KeyX: {keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x'},
+ KeyY: {keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y'},
+ KeyZ: {keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z'},
+ MetaLeft: {keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1},
+ MetaRight: {keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2},
+ ContextMenu: {keyCode: 93, code: 'ContextMenu', key: 'ContextMenu'},
+ NumpadMultiply: {
+ keyCode: 106,
+ code: 'NumpadMultiply',
+ key: '*',
+ location: 3,
+ },
+ NumpadAdd: {keyCode: 107, code: 'NumpadAdd', key: '+', location: 3},
+ NumpadSubtract: {
+ keyCode: 109,
+ code: 'NumpadSubtract',
+ key: '-',
+ location: 3,
+ },
+ NumpadDivide: {keyCode: 111, code: 'NumpadDivide', key: '/', location: 3},
+ F1: {keyCode: 112, code: 'F1', key: 'F1'},
+ F2: {keyCode: 113, code: 'F2', key: 'F2'},
+ F3: {keyCode: 114, code: 'F3', key: 'F3'},
+ F4: {keyCode: 115, code: 'F4', key: 'F4'},
+ F5: {keyCode: 116, code: 'F5', key: 'F5'},
+ F6: {keyCode: 117, code: 'F6', key: 'F6'},
+ F7: {keyCode: 118, code: 'F7', key: 'F7'},
+ F8: {keyCode: 119, code: 'F8', key: 'F8'},
+ F9: {keyCode: 120, code: 'F9', key: 'F9'},
+ F10: {keyCode: 121, code: 'F10', key: 'F10'},
+ F11: {keyCode: 122, code: 'F11', key: 'F11'},
+ F12: {keyCode: 123, code: 'F12', key: 'F12'},
+ F13: {keyCode: 124, code: 'F13', key: 'F13'},
+ F14: {keyCode: 125, code: 'F14', key: 'F14'},
+ F15: {keyCode: 126, code: 'F15', key: 'F15'},
+ F16: {keyCode: 127, code: 'F16', key: 'F16'},
+ F17: {keyCode: 128, code: 'F17', key: 'F17'},
+ F18: {keyCode: 129, code: 'F18', key: 'F18'},
+ F19: {keyCode: 130, code: 'F19', key: 'F19'},
+ F20: {keyCode: 131, code: 'F20', key: 'F20'},
+ F21: {keyCode: 132, code: 'F21', key: 'F21'},
+ F22: {keyCode: 133, code: 'F22', key: 'F22'},
+ F23: {keyCode: 134, code: 'F23', key: 'F23'},
+ F24: {keyCode: 135, code: 'F24', key: 'F24'},
+ NumLock: {keyCode: 144, code: 'NumLock', key: 'NumLock'},
+ ScrollLock: {keyCode: 145, code: 'ScrollLock', key: 'ScrollLock'},
+ AudioVolumeMute: {
+ keyCode: 173,
+ code: 'AudioVolumeMute',
+ key: 'AudioVolumeMute',
+ },
+ AudioVolumeDown: {
+ keyCode: 174,
+ code: 'AudioVolumeDown',
+ key: 'AudioVolumeDown',
+ },
+ AudioVolumeUp: {keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp'},
+ MediaTrackNext: {
+ keyCode: 176,
+ code: 'MediaTrackNext',
+ key: 'MediaTrackNext',
+ },
+ MediaTrackPrevious: {
+ keyCode: 177,
+ code: 'MediaTrackPrevious',
+ key: 'MediaTrackPrevious',
+ },
+ MediaStop: {keyCode: 178, code: 'MediaStop', key: 'MediaStop'},
+ MediaPlayPause: {
+ keyCode: 179,
+ code: 'MediaPlayPause',
+ key: 'MediaPlayPause',
+ },
+ Semicolon: {keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';'},
+ Equal: {keyCode: 187, code: 'Equal', shiftKey: '+', key: '='},
+ NumpadEqual: {keyCode: 187, code: 'NumpadEqual', key: '=', location: 3},
+ Comma: {keyCode: 188, code: 'Comma', shiftKey: '<', key: ','},
+ Minus: {keyCode: 189, code: 'Minus', shiftKey: '_', key: '-'},
+ Period: {keyCode: 190, code: 'Period', shiftKey: '>', key: '.'},
+ Slash: {keyCode: 191, code: 'Slash', shiftKey: '?', key: '/'},
+ Backquote: {keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`'},
+ BracketLeft: {keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '['},
+ Backslash: {keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\'},
+ BracketRight: {keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']'},
+ Quote: {keyCode: 222, code: 'Quote', shiftKey: '"', key: "'"},
+ AltGraph: {keyCode: 225, code: 'AltGraph', key: 'AltGraph'},
+ Props: {keyCode: 247, code: 'Props', key: 'CrSel'},
+ Cancel: {keyCode: 3, key: 'Cancel', code: 'Abort'},
+ Clear: {keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3},
+ Shift: {keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1},
+ Control: {keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1},
+ Alt: {keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1},
+ Accept: {keyCode: 30, key: 'Accept'},
+ ModeChange: {keyCode: 31, key: 'ModeChange'},
+ ' ': {keyCode: 32, key: ' ', code: 'Space'},
+ Print: {keyCode: 42, key: 'Print'},
+ Execute: {keyCode: 43, key: 'Execute', code: 'Open'},
+ '\u0000': {keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3},
+ a: {keyCode: 65, key: 'a', code: 'KeyA'},
+ b: {keyCode: 66, key: 'b', code: 'KeyB'},
+ c: {keyCode: 67, key: 'c', code: 'KeyC'},
+ d: {keyCode: 68, key: 'd', code: 'KeyD'},
+ e: {keyCode: 69, key: 'e', code: 'KeyE'},
+ f: {keyCode: 70, key: 'f', code: 'KeyF'},
+ g: {keyCode: 71, key: 'g', code: 'KeyG'},
+ h: {keyCode: 72, key: 'h', code: 'KeyH'},
+ i: {keyCode: 73, key: 'i', code: 'KeyI'},
+ j: {keyCode: 74, key: 'j', code: 'KeyJ'},
+ k: {keyCode: 75, key: 'k', code: 'KeyK'},
+ l: {keyCode: 76, key: 'l', code: 'KeyL'},
+ m: {keyCode: 77, key: 'm', code: 'KeyM'},
+ n: {keyCode: 78, key: 'n', code: 'KeyN'},
+ o: {keyCode: 79, key: 'o', code: 'KeyO'},
+ p: {keyCode: 80, key: 'p', code: 'KeyP'},
+ q: {keyCode: 81, key: 'q', code: 'KeyQ'},
+ r: {keyCode: 82, key: 'r', code: 'KeyR'},
+ s: {keyCode: 83, key: 's', code: 'KeyS'},
+ t: {keyCode: 84, key: 't', code: 'KeyT'},
+ u: {keyCode: 85, key: 'u', code: 'KeyU'},
+ v: {keyCode: 86, key: 'v', code: 'KeyV'},
+ w: {keyCode: 87, key: 'w', code: 'KeyW'},
+ x: {keyCode: 88, key: 'x', code: 'KeyX'},
+ y: {keyCode: 89, key: 'y', code: 'KeyY'},
+ z: {keyCode: 90, key: 'z', code: 'KeyZ'},
+ Meta: {keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1},
+ '*': {keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3},
+ '+': {keyCode: 107, key: '+', code: 'NumpadAdd', location: 3},
+ '-': {keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3},
+ '/': {keyCode: 111, key: '/', code: 'NumpadDivide', location: 3},
+ ';': {keyCode: 186, key: ';', code: 'Semicolon'},
+ '=': {keyCode: 187, key: '=', code: 'Equal'},
+ ',': {keyCode: 188, key: ',', code: 'Comma'},
+ '.': {keyCode: 190, key: '.', code: 'Period'},
+ '`': {keyCode: 192, key: '`', code: 'Backquote'},
+ '[': {keyCode: 219, key: '[', code: 'BracketLeft'},
+ '\\': {keyCode: 220, key: '\\', code: 'Backslash'},
+ ']': {keyCode: 221, key: ']', code: 'BracketRight'},
+ "'": {keyCode: 222, key: "'", code: 'Quote'},
+ Attn: {keyCode: 246, key: 'Attn'},
+ CrSel: {keyCode: 247, key: 'CrSel', code: 'Props'},
+ ExSel: {keyCode: 248, key: 'ExSel'},
+ EraseEof: {keyCode: 249, key: 'EraseEof'},
+ Play: {keyCode: 250, key: 'Play'},
+ ZoomOut: {keyCode: 251, key: 'ZoomOut'},
+ ')': {keyCode: 48, key: ')', code: 'Digit0'},
+ '!': {keyCode: 49, key: '!', code: 'Digit1'},
+ '@': {keyCode: 50, key: '@', code: 'Digit2'},
+ '#': {keyCode: 51, key: '#', code: 'Digit3'},
+ $: {keyCode: 52, key: '$', code: 'Digit4'},
+ '%': {keyCode: 53, key: '%', code: 'Digit5'},
+ '^': {keyCode: 54, key: '^', code: 'Digit6'},
+ '&': {keyCode: 55, key: '&', code: 'Digit7'},
+ '(': {keyCode: 57, key: '(', code: 'Digit9'},
+ A: {keyCode: 65, key: 'A', code: 'KeyA'},
+ B: {keyCode: 66, key: 'B', code: 'KeyB'},
+ C: {keyCode: 67, key: 'C', code: 'KeyC'},
+ D: {keyCode: 68, key: 'D', code: 'KeyD'},
+ E: {keyCode: 69, key: 'E', code: 'KeyE'},
+ F: {keyCode: 70, key: 'F', code: 'KeyF'},
+ G: {keyCode: 71, key: 'G', code: 'KeyG'},
+ H: {keyCode: 72, key: 'H', code: 'KeyH'},
+ I: {keyCode: 73, key: 'I', code: 'KeyI'},
+ J: {keyCode: 74, key: 'J', code: 'KeyJ'},
+ K: {keyCode: 75, key: 'K', code: 'KeyK'},
+ L: {keyCode: 76, key: 'L', code: 'KeyL'},
+ M: {keyCode: 77, key: 'M', code: 'KeyM'},
+ N: {keyCode: 78, key: 'N', code: 'KeyN'},
+ O: {keyCode: 79, key: 'O', code: 'KeyO'},
+ P: {keyCode: 80, key: 'P', code: 'KeyP'},
+ Q: {keyCode: 81, key: 'Q', code: 'KeyQ'},
+ R: {keyCode: 82, key: 'R', code: 'KeyR'},
+ S: {keyCode: 83, key: 'S', code: 'KeyS'},
+ T: {keyCode: 84, key: 'T', code: 'KeyT'},
+ U: {keyCode: 85, key: 'U', code: 'KeyU'},
+ V: {keyCode: 86, key: 'V', code: 'KeyV'},
+ W: {keyCode: 87, key: 'W', code: 'KeyW'},
+ X: {keyCode: 88, key: 'X', code: 'KeyX'},
+ Y: {keyCode: 89, key: 'Y', code: 'KeyY'},
+ Z: {keyCode: 90, key: 'Z', code: 'KeyZ'},
+ ':': {keyCode: 186, key: ':', code: 'Semicolon'},
+ '<': {keyCode: 188, key: '<', code: 'Comma'},
+ _: {keyCode: 189, key: '_', code: 'Minus'},
+ '>': {keyCode: 190, key: '>', code: 'Period'},
+ '?': {keyCode: 191, key: '?', code: 'Slash'},
+ '~': {keyCode: 192, key: '~', code: 'Backquote'},
+ '{': {keyCode: 219, key: '{', code: 'BracketLeft'},
+ '|': {keyCode: 220, key: '|', code: 'Backslash'},
+ '}': {keyCode: 221, key: '}', code: 'BracketRight'},
+ '"': {keyCode: 222, key: '"', code: 'Quote'},
+ SoftLeft: {key: 'SoftLeft', code: 'SoftLeft', location: 4},
+ SoftRight: {key: 'SoftRight', code: 'SoftRight', location: 4},
+ Camera: {keyCode: 44, key: 'Camera', code: 'Camera', location: 4},
+ Call: {key: 'Call', code: 'Call', location: 4},
+ EndCall: {keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4},
+ VolumeDown: {
+ keyCode: 182,
+ key: 'VolumeDown',
+ code: 'VolumeDown',
+ location: 4,
+ },
+ VolumeUp: {keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4},
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts
new file mode 100644
index 0000000000..46a937a88f
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Viewport.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @public
+ */
+export interface Viewport {
+ /**
+ * The page width in CSS pixels.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ */
+ width: number;
+ /**
+ * The page height in CSS pixels.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ */
+ height: number;
+ /**
+ * Specify device scale factor.
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info.
+ *
+ * @remarks
+ * Setting this value to `0` will reset this value to the system default.
+ *
+ * @defaultValue `1`
+ */
+ deviceScaleFactor?: number;
+ /**
+ * Whether the `meta viewport` tag is taken into account.
+ * @defaultValue `false`
+ */
+ isMobile?: boolean;
+ /**
+ * Specifies if the viewport is in landscape mode.
+ * @defaultValue `false`
+ */
+ isLandscape?: boolean;
+ /**
+ * Specify if the viewport supports touch events.
+ * @defaultValue `false`
+ */
+ hasTouch?: boolean;
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts
new file mode 100644
index 0000000000..d0c1e2a038
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts
@@ -0,0 +1,275 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import type {Realm} from '../api/Realm.js';
+import type {Poller} from '../injected/Poller.js';
+import {Deferred} from '../util/Deferred.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+import {stringifyFunction} from '../util/Function.js';
+
+import {TimeoutError} from './Errors.js';
+import {LazyArg} from './LazyArg.js';
+import type {HandleFor} from './types.js';
+
+/**
+ * @internal
+ */
+export interface WaitTaskOptions {
+ polling: 'raf' | 'mutation' | number;
+ root?: ElementHandle<Node>;
+ timeout: number;
+ signal?: AbortSignal;
+}
+
+/**
+ * @internal
+ */
+export class WaitTask<T = unknown> {
+ #world: Realm;
+ #polling: 'raf' | 'mutation' | number;
+ #root?: ElementHandle<Node>;
+
+ #fn: string;
+ #args: unknown[];
+
+ #timeout?: NodeJS.Timeout;
+ #timeoutError?: TimeoutError;
+
+ #result = Deferred.create<HandleFor<T>>();
+
+ #poller?: JSHandle<Poller<T>>;
+ #signal?: AbortSignal;
+ #reruns: AbortController[] = [];
+
+ constructor(
+ world: Realm,
+ options: WaitTaskOptions,
+ fn: ((...args: unknown[]) => Promise<T>) | string,
+ ...args: unknown[]
+ ) {
+ this.#world = world;
+ this.#polling = options.polling;
+ this.#root = options.root;
+ this.#signal = options.signal;
+ this.#signal?.addEventListener(
+ 'abort',
+ () => {
+ void this.terminate(this.#signal?.reason);
+ },
+ {
+ once: true,
+ }
+ );
+
+ switch (typeof fn) {
+ case 'string':
+ this.#fn = `() => {return (${fn});}`;
+ break;
+ default:
+ this.#fn = stringifyFunction(fn);
+ break;
+ }
+ this.#args = args;
+
+ this.#world.taskManager.add(this);
+
+ if (options.timeout) {
+ this.#timeoutError = new TimeoutError(
+ `Waiting failed: ${options.timeout}ms exceeded`
+ );
+ this.#timeout = setTimeout(() => {
+ void this.terminate(this.#timeoutError);
+ }, options.timeout);
+ }
+
+ void this.rerun();
+ }
+
+ get result(): Promise<HandleFor<T>> {
+ return this.#result.valueOrThrow();
+ }
+
+ async rerun(): Promise<void> {
+ for (const prev of this.#reruns) {
+ prev.abort();
+ }
+ this.#reruns.length = 0;
+ const controller = new AbortController();
+ this.#reruns.push(controller);
+ try {
+ 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>;
+ });
+ },
+ LazyArg.create(context => {
+ return context.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);
+ },
+ LazyArg.create(context => {
+ return context.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);
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ this.#polling,
+ this.#fn,
+ ...this.#args
+ );
+ break;
+ }
+
+ await this.#poller.evaluate(poller => {
+ void poller.start();
+ });
+
+ const result = await this.#poller.evaluateHandle(poller => {
+ return poller.result();
+ });
+ this.#result.resolve(result);
+
+ await this.terminate();
+ } catch (error) {
+ if (controller.signal.aborted) {
+ return;
+ }
+ const badError = this.getBadError(error);
+ if (badError) {
+ await this.terminate(badError);
+ }
+ }
+ }
+
+ async terminate(error?: Error): Promise<void> {
+ this.#world.taskManager.delete(this);
+
+ 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): Error | undefined {
+ if (isErrorLike(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;
+ }
+
+ // Errors coming from WebDriver BiDi. TODO: Adjust messages after
+ // https://github.com/w3c/webdriver-bidi/issues/540 is resolved.
+ if (
+ error.message.includes(
+ "AbortError: Actor 'MessageHandlerFrame' destroyed"
+ )
+ ) {
+ return;
+ }
+
+ return error;
+ }
+
+ return new Error('WaitTask failed with an error', {
+ cause: 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) {
+ void task.terminate(error);
+ }
+ this.#tasks.clear();
+ }
+
+ async rerunAll(): Promise<void> {
+ await Promise.all(
+ [...this.#tasks].map(task => {
+ return task.rerun();
+ })
+ );
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts
new file mode 100644
index 0000000000..b6e3a67bad
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ QueryHandler,
+ type QuerySelectorAll,
+ type QuerySelector,
+} from './QueryHandler.js';
+
+/**
+ * @internal
+ */
+export class XPathQueryHandler extends QueryHandler {
+ static override querySelectorAll: QuerySelectorAll = (
+ element,
+ selector,
+ {xpathQuerySelectorAll}
+ ) => {
+ return xpathQuerySelectorAll(element, selector);
+ };
+
+ static override querySelector: QuerySelector = (
+ element: Node,
+ selector: string,
+ {xpathQuerySelectorAll}
+ ) => {
+ for (const result of xpathQuerySelectorAll(element, selector, 1)) {
+ return result;
+ }
+ return null;
+ };
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
new file mode 100644
index 0000000000..6ef8925605
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './BrowserWebSocketTransport.js';
+export * from './CallbackRegistry.js';
+export * from './Configuration.js';
+export * from './ConnectionTransport.js';
+export * from './ConnectOptions.js';
+export * from './ConsoleMessage.js';
+export * from './CustomQueryHandler.js';
+export * from './Debug.js';
+export * from './Device.js';
+export * from './Errors.js';
+export * from './EventEmitter.js';
+export * from './fetch.js';
+export * from './FileChooser.js';
+export * from './GetQueryHandler.js';
+export * from './HandleIterator.js';
+export * from './LazyArg.js';
+export * from './NetworkManagerEvents.js';
+export * from './PDFOptions.js';
+export * from './PierceQueryHandler.js';
+export * from './PQueryHandler.js';
+export * from './Product.js';
+export * from './Puppeteer.js';
+export * from './QueryHandler.js';
+export * from './ScriptInjector.js';
+export * from './SecurityDetails.js';
+export * from './TaskQueue.js';
+export * from './TextQueryHandler.js';
+export * from './TimeoutSettings.js';
+export * from './types.js';
+export * from './USKeyboardLayout.js';
+export * from './util.js';
+export * from './Viewport.js';
+export * from './WaitTask.js';
+export * from './XPathQueryHandler.js';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
new file mode 100644
index 0000000000..6c7a2b451c
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Gets the global version if we're in the browser, else loads the node-fetch module.
+ *
+ * @internal
+ */
+export const getFetch = async (): Promise<typeof fetch> => {
+ return (globalThis as any).fetch || (await import('cross-fetch')).fetch;
+};
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts
new file mode 100644
index 0000000000..3f2cf5d4f3
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import type {JSHandle} from '../api/JSHandle.js';
+
+import type {LazyArg} from './LazyArg.js';
+
+/**
+ * @public
+ */
+export type AwaitablePredicate<T> = (value: T) => Awaitable<boolean>;
+
+/**
+ * @public
+ */
+export interface Moveable {
+ /**
+ * Moves the resource when 'using'.
+ */
+ move(): this;
+}
+
+/**
+ * @internal
+ */
+export interface Disposed {
+ get disposed(): boolean;
+}
+
+/**
+ * @internal
+ */
+export interface BindingPayload {
+ type: string;
+ name: string;
+ seq: number;
+ args: unknown[];
+ /**
+ * Determines whether the arguments of the payload are trivial.
+ */
+ isTrivial: boolean;
+}
+
+/**
+ * @internal
+ */
+export type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>;
+
+/**
+ * @public
+ */
+export type AwaitableIterable<T> = Iterable<T> | AsyncIterable<T>;
+
+/**
+ * @public
+ */
+export type Awaitable<T> = T | PromiseLike<T>;
+
+/**
+ * @public
+ */
+export type HandleFor<T> = T extends Node ? ElementHandle<T> : JSHandle<T>;
+
+/**
+ * @public
+ */
+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;
+
+/**
+ * @internal
+ */
+export type InnerLazyParams<T extends unknown[]> = {
+ [K in keyof T]: FlattenLazyArg<T[K]>;
+};
+
+/**
+ * @public
+ */
+export type InnerParams<T extends unknown[]> = {
+ [K in keyof T]: FlattenHandle<T[K]>;
+};
+
+/**
+ * @public
+ */
+export type ElementFor<
+ TagName extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
+> = TagName extends keyof HTMLElementTagNameMap
+ ? HTMLElementTagNameMap[TagName]
+ : TagName extends keyof SVGElementTagNameMap
+ ? SVGElementTagNameMap[TagName]
+ : never;
+
+/**
+ * @public
+ */
+export type EvaluateFunc<T extends unknown[]> = (
+ ...params: InnerParams<T>
+) => Awaitable<unknown>;
+
+/**
+ * @public
+ */
+export type EvaluateFuncWith<V, T extends unknown[]> = (
+ ...params: [V, ...InnerParams<T>]
+) => Awaitable<unknown>;
+
+/**
+ * @public
+ */
+export type NodeFor<ComplexSelector extends string> =
+ TypeSelectorOfComplexSelector<ComplexSelector> extends infer TypeSelector
+ ? TypeSelector extends
+ | keyof HTMLElementTagNameMap
+ | keyof SVGElementTagNameMap
+ ? ElementFor<TypeSelector>
+ : Element
+ : never;
+
+type TypeSelectorOfComplexSelector<ComplexSelector extends string> =
+ CompoundSelectorsOfComplexSelector<ComplexSelector> extends infer CompoundSelectors
+ ? CompoundSelectors extends NonEmptyReadonlyArray<string>
+ ? Last<CompoundSelectors> extends infer LastCompoundSelector
+ ? LastCompoundSelector extends string
+ ? TypeSelectorOfCompoundSelector<LastCompoundSelector>
+ : never
+ : never
+ : unknown
+ : never;
+
+type TypeSelectorOfCompoundSelector<CompoundSelector extends string> =
+ SplitWithDelemiters<
+ CompoundSelector,
+ BeginSubclassSelectorTokens
+ > extends infer CompoundSelectorTokens
+ ? CompoundSelectorTokens extends [infer TypeSelector, ...any[]]
+ ? TypeSelector extends ''
+ ? unknown
+ : TypeSelector
+ : never
+ : never;
+
+type Last<Arr extends NonEmptyReadonlyArray<unknown>> = Arr extends [
+ infer Head,
+ ...infer Tail,
+]
+ ? Tail extends NonEmptyReadonlyArray<unknown>
+ ? Last<Tail>
+ : Head
+ : never;
+
+type NonEmptyReadonlyArray<T> = [T, ...(readonly T[])];
+
+type CompoundSelectorsOfComplexSelector<ComplexSelector extends string> =
+ SplitWithDelemiters<
+ ComplexSelector,
+ CombinatorTokens
+ > extends infer IntermediateTokens
+ ? IntermediateTokens extends readonly string[]
+ ? Drop<IntermediateTokens, ''>
+ : never
+ : never;
+
+type SplitWithDelemiters<
+ Input extends string,
+ Delemiters extends readonly string[],
+> = Delemiters extends [infer FirstDelemiter, ...infer RestDelemiters]
+ ? FirstDelemiter extends string
+ ? RestDelemiters extends readonly string[]
+ ? FlatmapSplitWithDelemiters<Split<Input, FirstDelemiter>, RestDelemiters>
+ : never
+ : never
+ : [Input];
+
+type BeginSubclassSelectorTokens = ['.', '#', '[', ':'];
+
+type CombinatorTokens = [' ', '>', '+', '~', '|', '|'];
+
+type Drop<
+ Arr extends readonly unknown[],
+ Remove,
+ Acc extends unknown[] = [],
+> = Arr extends [infer Head, ...infer Tail]
+ ? Head extends Remove
+ ? Drop<Tail, Remove>
+ : Drop<Tail, Remove, [...Acc, Head]>
+ : Acc;
+
+type FlatmapSplitWithDelemiters<
+ Inputs extends readonly string[],
+ Delemiters extends readonly string[],
+ Acc extends string[] = [],
+> = Inputs extends [infer FirstInput, ...infer RestInputs]
+ ? FirstInput extends string
+ ? RestInputs extends readonly string[]
+ ? FlatmapSplitWithDelemiters<
+ RestInputs,
+ Delemiters,
+ [...Acc, ...SplitWithDelemiters<FirstInput, Delemiters>]
+ >
+ : Acc
+ : Acc
+ : Acc;
+
+type Split<
+ Input extends string,
+ Delimiter extends string,
+ Acc extends string[] = [],
+> = Input extends `${infer Prefix}${Delimiter}${infer Suffix}`
+ ? Split<Suffix, Delimiter, [...Acc, Prefix]>
+ : [...Acc, Input];
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
new file mode 100644
index 0000000000..2c8f76f664
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts
@@ -0,0 +1,447 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type FS from 'fs/promises';
+import type {Readable} from 'stream';
+
+import {map, NEVER, Observable, timer} from '../../third_party/rxjs/rxjs.js';
+import type {CDPSession} from '../api/CDPSession.js';
+import {isNode} from '../environment.js';
+import {assert} from '../util/assert.js';
+import {isErrorLike} from '../util/ErrorLike.js';
+
+import {debug} from './Debug.js';
+import {TimeoutError} from './Errors.js';
+import type {EventEmitter, EventType} from './EventEmitter.js';
+import type {
+ LowerCasePaperFormat,
+ ParsedPDFOptions,
+ PDFOptions,
+} from './PDFOptions.js';
+import {paperFormats} from './PDFOptions.js';
+
+/**
+ * @internal
+ */
+export const debugError = debug('puppeteer:error');
+
+/**
+ * @internal
+ */
+export const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
+
+/**
+ * @internal
+ */
+const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
+
+/**
+ * @internal
+ */
+export class PuppeteerURL {
+ static INTERNAL_URL = 'pptr:internal';
+
+ static fromCallSite(
+ functionName: string,
+ site: NodeJS.CallSite
+ ): PuppeteerURL {
+ const url = new PuppeteerURL();
+ url.#functionName = functionName;
+ url.#siteString = site.toString();
+ return url;
+ }
+
+ static parse = (url: string): PuppeteerURL => {
+ url = url.slice('pptr:'.length);
+ const [functionName = '', siteString = ''] = url.split(';');
+ const puppeteerUrl = new PuppeteerURL();
+ puppeteerUrl.#functionName = functionName;
+ puppeteerUrl.#siteString = decodeURIComponent(siteString);
+ return puppeteerUrl;
+ };
+
+ static isPuppeteerURL = (url: string): boolean => {
+ return url.startsWith('pptr:');
+ };
+
+ #functionName!: string;
+ #siteString!: string;
+
+ get functionName(): string {
+ return this.#functionName;
+ }
+
+ get siteString(): string {
+ return this.#siteString;
+ }
+
+ toString(): string {
+ return `pptr:${[
+ this.#functionName,
+ encodeURIComponent(this.#siteString),
+ ].join(';')}`;
+ }
+}
+
+/**
+ * @internal
+ */
+export const withSourcePuppeteerURLIfNone = <T extends NonNullable<unknown>>(
+ functionName: string,
+ object: T
+): T => {
+ if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
+ return object;
+ }
+ const original = Error.prepareStackTrace;
+ Error.prepareStackTrace = (_, stack) => {
+ // First element is the function.
+ // Second element is the caller of this function.
+ // Third element is the caller of the caller of this function
+ // which is precisely what we want.
+ return stack[2];
+ };
+ const site = new Error().stack as unknown as NodeJS.CallSite;
+ Error.prepareStackTrace = original;
+ return Object.assign(object, {
+ [SOURCE_URL]: PuppeteerURL.fromCallSite(functionName, site),
+ });
+};
+
+/**
+ * @internal
+ */
+export const getSourcePuppeteerURLIfAvailable = <
+ T extends NonNullable<unknown>,
+>(
+ object: T
+): PuppeteerURL | undefined => {
+ if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
+ return object[SOURCE_URL as keyof T] as PuppeteerURL;
+ }
+ return undefined;
+};
+
+/**
+ * @internal
+ */
+export const isString = (obj: unknown): obj is string => {
+ return typeof obj === 'string' || obj instanceof String;
+};
+
+/**
+ * @internal
+ */
+export const isNumber = (obj: unknown): obj is number => {
+ return typeof obj === 'number' || obj instanceof Number;
+};
+
+/**
+ * @internal
+ */
+export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
+ return typeof obj === 'object' && obj?.constructor === Object;
+};
+
+/**
+ * @internal
+ */
+export const isRegExp = (obj: unknown): obj is RegExp => {
+ return typeof obj === 'object' && obj?.constructor === RegExp;
+};
+
+/**
+ * @internal
+ */
+export const isDate = (obj: unknown): obj is Date => {
+ return typeof obj === 'object' && obj?.constructor === Date;
+};
+
+/**
+ * @internal
+ */
+export function evaluationString(
+ fun: Function | string,
+ ...args: unknown[]
+): string {
+ if (isString(fun)) {
+ assert(args.length === 0, 'Cannot evaluate a string with arguments');
+ return fun;
+ }
+
+ function serializeArgument(arg: unknown): string {
+ if (Object.is(arg, undefined)) {
+ return 'undefined';
+ }
+ return JSON.stringify(arg);
+ }
+
+ return `(${fun})(${args.map(serializeArgument).join(',')})`;
+}
+
+/**
+ * @internal
+ */
+let fs: typeof FS | null = null;
+/**
+ * @internal
+ */
+export async function importFSPromises(): Promise<typeof FS> {
+ if (!fs) {
+ try {
+ fs = await import('fs/promises');
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ 'Cannot write to a path outside of a Node-like environment.'
+ );
+ }
+ throw error;
+ }
+ }
+ return fs;
+}
+
+/**
+ * @internal
+ */
+export async function getReadableAsBuffer(
+ readable: Readable,
+ path?: string
+): Promise<Buffer | null> {
+ const buffers = [];
+ if (path) {
+ const fs = await importFSPromises();
+ const fileHandle = await fs.open(path, 'w+');
+ try {
+ for await (const chunk of readable) {
+ buffers.push(chunk);
+ await fileHandle.writeFile(chunk);
+ }
+ } finally {
+ await fileHandle.close();
+ }
+ } else {
+ for await (const chunk of readable) {
+ buffers.push(chunk);
+ }
+ }
+ try {
+ return Buffer.concat(buffers);
+ } catch (error) {
+ return null;
+ }
+}
+
+/**
+ * @internal
+ */
+export async function getReadableFromProtocolStream(
+ client: CDPSession,
+ handle: string
+): Promise<Readable> {
+ // TODO: Once Node 18 becomes the lowest supported version, we can migrate to
+ // ReadableStream.
+ if (!isNode) {
+ throw new Error('Cannot create a stream outside of Node.js environment.');
+ }
+
+ const {Readable} = await import('stream');
+
+ let eof = false;
+ return new Readable({
+ async read(size: number) {
+ if (eof) {
+ return;
+ }
+
+ try {
+ const response = await client.send('IO.read', {handle, size});
+ this.push(response.data, response.base64Encoded ? 'base64' : undefined);
+ if (response.eof) {
+ eof = true;
+ await client.send('IO.close', {handle});
+ this.push(null);
+ }
+ } catch (error) {
+ if (isErrorLike(error)) {
+ this.destroy(error);
+ return;
+ }
+ throw error;
+ }
+ },
+ });
+}
+
+/**
+ * @internal
+ */
+export function validateDialogType(
+ type: string
+): 'alert' | 'confirm' | 'prompt' | 'beforeunload' {
+ let dialogType = null;
+ const validDialogTypes = new Set([
+ 'alert',
+ 'confirm',
+ 'prompt',
+ 'beforeunload',
+ ]);
+
+ if (validDialogTypes.has(type)) {
+ dialogType = type;
+ }
+ assert(dialogType, `Unknown javascript dialog type: ${type}`);
+ return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload';
+}
+
+/**
+ * @internal
+ */
+export function timeout(ms: number): Observable<never> {
+ return ms === 0
+ ? NEVER
+ : timer(ms).pipe(
+ map(() => {
+ throw new TimeoutError(`Timed out after waiting ${ms}ms`);
+ })
+ );
+}
+
+/**
+ * @internal
+ */
+export const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
+
+/**
+ * @internal
+ */
+export const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
+/**
+ * @internal
+ */
+export function getSourceUrlComment(url: string): string {
+ return `//# sourceURL=${url}`;
+}
+
+/**
+ * @internal
+ */
+export const NETWORK_IDLE_TIME = 500;
+
+/**
+ * @internal
+ */
+export function parsePDFOptions(
+ options: PDFOptions = {},
+ lengthUnit: 'in' | 'cm' = 'in'
+): ParsedPDFOptions {
+ const defaults: Omit<ParsedPDFOptions, 'width' | 'height' | 'margin'> = {
+ scale: 1,
+ displayHeaderFooter: false,
+ headerTemplate: '',
+ footerTemplate: '',
+ printBackground: false,
+ landscape: false,
+ pageRanges: '',
+ preferCSSPageSize: false,
+ omitBackground: false,
+ tagged: false,
+ };
+
+ let width = 8.5;
+ let height = 11;
+ if (options.format) {
+ const format =
+ paperFormats[options.format.toLowerCase() as LowerCasePaperFormat];
+ assert(format, 'Unknown paper format: ' + options.format);
+ width = format.width;
+ height = format.height;
+ } else {
+ width = convertPrintParameterToInches(options.width, lengthUnit) ?? width;
+ height =
+ convertPrintParameterToInches(options.height, lengthUnit) ?? height;
+ }
+
+ const margin = {
+ top: convertPrintParameterToInches(options.margin?.top, lengthUnit) || 0,
+ left: convertPrintParameterToInches(options.margin?.left, lengthUnit) || 0,
+ bottom:
+ convertPrintParameterToInches(options.margin?.bottom, lengthUnit) || 0,
+ right:
+ convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0,
+ };
+
+ return {
+ ...defaults,
+ ...options,
+ width,
+ height,
+ margin,
+ };
+}
+
+/**
+ * @internal
+ */
+export const unitToPixels = {
+ px: 1,
+ in: 96,
+ cm: 37.8,
+ mm: 3.78,
+};
+
+function convertPrintParameterToInches(
+ parameter?: string | number,
+ lengthUnit: 'in' | 'cm' = 'in'
+): number | undefined {
+ if (typeof parameter === 'undefined') {
+ return undefined;
+ }
+ let pixels;
+ if (isNumber(parameter)) {
+ // Treat numbers as pixel values to be aligned with phantom's paperSize.
+ pixels = parameter;
+ } else if (isString(parameter)) {
+ const text = parameter;
+ let unit = text.substring(text.length - 2).toLowerCase();
+ let valueText = '';
+ if (unit in unitToPixels) {
+ valueText = text.substring(0, text.length - 2);
+ } else {
+ // In case of unknown unit try to parse the whole parameter as number of pixels.
+ // This is consistent with phantom's paperSize behavior.
+ unit = 'px';
+ valueText = text;
+ }
+ const value = Number(valueText);
+ assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
+ pixels = value * unitToPixels[unit as keyof typeof unitToPixels];
+ } else {
+ throw new Error(
+ 'page.pdf() Cannot handle parameter type: ' + typeof parameter
+ );
+ }
+ return pixels / unitToPixels[lengthUnit];
+}
+
+/**
+ * @internal
+ */
+export function fromEmitterEvent<
+ Events extends Record<EventType, unknown>,
+ Event extends keyof Events,
+>(emitter: EventEmitter<Events>, eventName: Event): Observable<Events[Event]> {
+ return new Observable(subscriber => {
+ const listener = (event: Events[Event]) => {
+ subscriber.next(event);
+ };
+ emitter.on(eventName, listener);
+ return () => {
+ emitter.off(eventName, listener);
+ };
+ });
+}