chore: use private fields (#8506)

This commit is contained in:
jrandolf 2022-06-13 11:16:25 +02:00 committed by GitHub
parent 733cbecf48
commit 6c960115a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1830 additions and 1757 deletions

View File

@ -1,3 +1,12 @@
// TODO: Enable this at some point.
// const RESTRICTED_UNDERSCORED_IDENTIFIERS = [
// 'PropertyDefinition > Identifier[name=/^_[a-z].*$/]',
// ].map((selector) => ({
// selector,
// message:
// 'Use private fields (fields prefixed with #) and an appropriate getter/setter.',
// }));
module.exports = {
root: true,
env: {
@ -15,14 +24,6 @@ module.exports = {
// Error if files are not formatted with Prettier correctly.
'prettier/prettier': 2,
// syntax preferences
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'spaced-comment': [
2,
'always',
@ -116,6 +117,12 @@ module.exports = {
},
],
'import/extensions': ['error', 'ignorePackages'],
'no-restricted-syntax': [
'error',
// Don't allow underscored declarations on camelCased variables/properties.
// ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
],
},
overrides: [
{
@ -144,8 +151,6 @@ module.exports = {
// We don't require explicit return types on basic functions or
// dummy functions in tests, for example
'@typescript-eslint/explicit-function-return-type': 0,
// We know it's bad and use it very sparingly but it's needed :(
'@typescript-eslint/ban-ts-ignore': 0,
// We allow non-null assertions if the value was asserted using `assert` API.
'@typescript-eslint/no-non-null-assertion': 0,
/**
@ -176,6 +181,18 @@ module.exports = {
],
// By default this is a warning but we want it to error.
'@typescript-eslint/explicit-module-boundary-types': 2,
'no-restricted-syntax': [
'error',
{
// Never use `require` in TypeScript since they are transpiled out.
selector: "CallExpression[callee.name='require']",
message: '`require` statements are not allowed. Use `import`.',
},
// Don't allow underscored declarations on camelCased variables/properties.
// ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
],
},
},
],

View File

@ -130,13 +130,13 @@ export interface SnapshotOptions {
* @public
*/
export class Accessibility {
private _client: CDPSession;
#client: CDPSession;
/**
* @internal
*/
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
/**
@ -182,10 +182,10 @@ export class Accessibility {
options: SnapshotOptions = {}
): Promise<SerializedAXNode | null> {
const { interestingOnly = true, root = null } = options;
const { nodes } = await this._client.send('Accessibility.getFullAXTree');
const { nodes } = await this.#client.send('Accessibility.getFullAXTree');
let backendNodeId: number | undefined;
if (root) {
const { node } = await this._client.send('DOM.describeNode', {
const { node } = await this.#client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId,
});
backendNodeId = node.backendNodeId;
@ -238,53 +238,53 @@ class AXNode {
public payload: Protocol.Accessibility.AXNode;
public children: AXNode[] = [];
private _richlyEditable = false;
private _editable = false;
private _focusable = false;
private _hidden = false;
private _name: string;
private _role: string;
private _ignored: boolean;
private _cachedHasFocusableChild?: boolean;
#richlyEditable = false;
#editable = false;
#focusable = false;
#hidden = false;
#name: string;
#role: string;
#ignored: boolean;
#cachedHasFocusableChild?: boolean;
constructor(payload: Protocol.Accessibility.AXNode) {
this.payload = payload;
this._name = this.payload.name ? this.payload.name.value : '';
this._role = this.payload.role ? this.payload.role.value : 'Unknown';
this._ignored = this.payload.ignored;
this.#name = this.payload.name ? this.payload.name.value : '';
this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
this.#ignored = this.payload.ignored;
for (const property of this.payload.properties || []) {
if (property.name === 'editable') {
this._richlyEditable = property.value.value === 'richtext';
this._editable = true;
this.#richlyEditable = property.value.value === 'richtext';
this.#editable = true;
}
if (property.name === 'focusable') this._focusable = property.value.value;
if (property.name === 'hidden') this._hidden = property.value.value;
if (property.name === 'focusable') this.#focusable = property.value.value;
if (property.name === 'hidden') this.#hidden = property.value.value;
}
}
private _isPlainTextField(): boolean {
if (this._richlyEditable) return false;
if (this._editable) return true;
return this._role === 'textbox' || this._role === 'searchbox';
#isPlainTextField(): boolean {
if (this.#richlyEditable) return false;
if (this.#editable) return true;
return this.#role === 'textbox' || this.#role === 'searchbox';
}
private _isTextOnlyObject(): boolean {
const role = this._role;
#isTextOnlyObject(): boolean {
const role = this.#role;
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
}
private _hasFocusableChild(): boolean {
if (this._cachedHasFocusableChild === undefined) {
this._cachedHasFocusableChild = false;
#hasFocusableChild(): boolean {
if (this.#cachedHasFocusableChild === undefined) {
this.#cachedHasFocusableChild = false;
for (const child of this.children) {
if (child._focusable || child._hasFocusableChild()) {
this._cachedHasFocusableChild = true;
if (child.#focusable || child.#hasFocusableChild()) {
this.#cachedHasFocusableChild = true;
break;
}
}
}
return this._cachedHasFocusableChild;
return this.#cachedHasFocusableChild;
}
public find(predicate: (x: AXNode) => boolean): AXNode | null {
@ -303,13 +303,13 @@ class AXNode {
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (this._isPlainTextField() || this._isTextOnlyObject()) return true;
if (this.#isPlainTextField() || this.#isTextOnlyObject()) return true;
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
// (Note that whilst ARIA buttons can have only presentational children, HTML5
// buttons are allowed to have content.)
switch (this._role) {
switch (this.#role) {
case 'doc-cover':
case 'graphics-symbol':
case 'img':
@ -324,14 +324,14 @@ class AXNode {
}
// Here and below: Android heuristics
if (this._hasFocusableChild()) return false;
if (this._focusable && this._name) return true;
if (this._role === 'heading' && this._name) return true;
if (this.#hasFocusableChild()) return false;
if (this.#focusable && this.#name) return true;
if (this.#role === 'heading' && this.#name) return true;
return false;
}
public isControl(): boolean {
switch (this._role) {
switch (this.#role) {
case 'button':
case 'checkbox':
case 'ColorWell':
@ -360,10 +360,10 @@ class AXNode {
}
public isInteresting(insideControl: boolean): boolean {
const role = this._role;
if (role === 'Ignored' || this._hidden || this._ignored) return false;
const role = this.#role;
if (role === 'Ignored' || this.#hidden || this.#ignored) return false;
if (this._focusable || this._richlyEditable) return true;
if (this.#focusable || this.#richlyEditable) return true;
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl()) return true;
@ -371,7 +371,7 @@ class AXNode {
// A non focusable child of a control is not interesting
if (insideControl) return false;
return this.isLeafNode() && !!this._name;
return this.isLeafNode() && !!this.#name;
}
public serialize(): SerializedAXNode {
@ -384,7 +384,7 @@ class AXNode {
properties.set('description', this.payload.description.value);
const node: SerializedAXNode = {
role: this._role,
role: this.#role,
};
type UserStringProperty =
@ -440,7 +440,7 @@ class AXNode {
// RootWebArea's treat focus differently than other nodes. They report whether
// their frame has focus, not whether focus is specifically on the root
// node.
if (booleanProperty === 'focused' && this._role === 'RootWebArea')
if (booleanProperty === 'focused' && this.#role === 'RootWebArea')
continue;
const value = getBooleanPropertyValue(booleanProperty);
if (!value) continue;

View File

@ -107,7 +107,7 @@ const waitFor = async (
return element;
},
};
return domWorld.waitForSelectorInPage(
return domWorld._waitForSelectorInPage(
(_: Element, selector: string) =>
(
globalThis as unknown as { ariaQuerySelector(selector: string): void }
@ -146,7 +146,7 @@ const queryAllArray = async (
/**
* @internal
*/
export const ariaHandler: InternalQueryHandler = {
export const _ariaHandler: InternalQueryHandler = {
queryOne,
waitFor,
queryAll,

View File

@ -217,7 +217,7 @@ export class Browser extends EventEmitter {
/**
* @internal
*/
static async create(
static async _create(
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
@ -240,22 +240,25 @@ export class Browser extends EventEmitter {
await connection.send('Target.setDiscoverTargets', { discover: true });
return browser;
}
private _ignoreHTTPSErrors: boolean;
private _defaultViewport?: Viewport | null;
private _process?: ChildProcess;
private _connection: Connection;
private _closeCallback: BrowserCloseCallback;
private _targetFilterCallback: TargetFilterCallback;
private _isPageTargetCallback!: IsPageTargetCallback;
private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
private _screenshotTaskQueue: TaskQueue;
private _ignoredTargets = new Set<string>();
#ignoreHTTPSErrors: boolean;
#defaultViewport?: Viewport | null;
#process?: ChildProcess;
#connection: Connection;
#closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: BrowserContext;
#contexts: Map<string, BrowserContext>;
#screenshotTaskQueue: TaskQueue;
#targets: Map<string, Target>;
#ignoredTargets = new Set<string>();
/**
* @internal
* Used in Target.ts directly so cannot be marked private.
*/
_targets: Map<string, Target>;
get _targets(): Map<string, Target> {
return this.#targets;
}
/**
* @internal
@ -271,35 +274,35 @@ export class Browser extends EventEmitter {
isPageTargetCallback?: IsPageTargetCallback
) {
super();
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport;
this._process = process;
this._screenshotTaskQueue = new TaskQueue();
this._connection = connection;
this._closeCallback = closeCallback || function (): void {};
this._targetFilterCallback = targetFilterCallback || ((): boolean => true);
this._setIsPageTargetCallback(isPageTargetCallback);
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport;
this.#process = process;
this.#screenshotTaskQueue = new TaskQueue();
this.#connection = connection;
this.#closeCallback = closeCallback || function (): void {};
this.#targetFilterCallback = targetFilterCallback || ((): boolean => true);
this.#setIsPageTargetCallback(isPageTargetCallback);
this._defaultContext = new BrowserContext(this._connection, this);
this._contexts = new Map();
this.#defaultContext = new BrowserContext(this.#connection, this);
this.#contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(
this.#contexts.set(
contextId,
new BrowserContext(this._connection, this, contextId)
new BrowserContext(this.#connection, this, contextId)
);
this._targets = new Map();
this._connection.on(ConnectionEmittedEvents.Disconnected, () =>
this.#targets = new Map();
this.#connection.on(ConnectionEmittedEvents.Disconnected, () =>
this.emit(BrowserEmittedEvents.Disconnected)
);
this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
this._connection.on(
this.#connection.on('Target.targetCreated', this.#targetCreated.bind(this));
this.#connection.on(
'Target.targetDestroyed',
this._targetDestroyed.bind(this)
this.#targetDestroyed.bind(this)
);
this._connection.on(
this.#connection.on(
'Target.targetInfoChanged',
this._targetInfoChanged.bind(this)
this.#targetInfoChanged.bind(this)
);
}
@ -308,14 +311,11 @@ export class Browser extends EventEmitter {
* {@link Puppeteer.connect}.
*/
process(): ChildProcess | null {
return this._process ?? null;
return this.#process ?? null;
}
/**
* @internal
*/
_setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
this._isPageTargetCallback =
#setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
this.#isPageTargetCallback =
isPageTargetCallback ||
((target: Protocol.Target.TargetInfo): boolean => {
return (
@ -330,7 +330,7 @@ export class Browser extends EventEmitter {
* @internal
*/
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
return this._isPageTargetCallback;
return this.#isPageTargetCallback;
}
/**
@ -355,7 +355,7 @@ export class Browser extends EventEmitter {
): Promise<BrowserContext> {
const { proxyServer, proxyBypassList } = options;
const { browserContextId } = await this._connection.send(
const { browserContextId } = await this.#connection.send(
'Target.createBrowserContext',
{
proxyServer,
@ -363,11 +363,11 @@ export class Browser extends EventEmitter {
}
);
const context = new BrowserContext(
this._connection,
this.#connection,
this,
browserContextId
);
this._contexts.set(browserContextId, context);
this.#contexts.set(browserContextId, context);
return context;
}
@ -376,64 +376,63 @@ export class Browser extends EventEmitter {
* return a single instance of {@link BrowserContext}.
*/
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
}
/**
* Returns the default browser context. The default browser context cannot be closed.
*/
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
return this.#defaultContext;
}
/**
* @internal
* Used by BrowserContext directly so cannot be marked private.
*/
async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) {
return;
}
await this._connection.send('Target.disposeBrowserContext', {
await this.#connection.send('Target.disposeBrowserContext', {
browserContextId: contextId,
});
this._contexts.delete(contextId);
this.#contexts.delete(contextId);
}
private async _targetCreated(
async #targetCreated(
event: Protocol.Target.TargetCreatedEvent
): Promise<void> {
const targetInfo = event.targetInfo;
const { browserContextId } = targetInfo;
const context =
browserContextId && this._contexts.has(browserContextId)
? this._contexts.get(browserContextId)
: this._defaultContext;
browserContextId && this.#contexts.has(browserContextId)
? this.#contexts.get(browserContextId)
: this.#defaultContext;
if (!context) {
throw new Error('Missing browser context');
}
const shouldAttachToTarget = this._targetFilterCallback(targetInfo);
const shouldAttachToTarget = this.#targetFilterCallback(targetInfo);
if (!shouldAttachToTarget) {
this._ignoredTargets.add(targetInfo.targetId);
this.#ignoredTargets.add(targetInfo.targetId);
return;
}
const target = new Target(
targetInfo,
context,
() => this._connection.createSession(targetInfo),
this._ignoreHTTPSErrors,
this._defaultViewport ?? null,
this._screenshotTaskQueue,
this._isPageTargetCallback
() => this.#connection.createSession(targetInfo),
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null,
this.#screenshotTaskQueue,
this.#isPageTargetCallback
);
assert(
!this._targets.has(event.targetInfo.targetId),
!this.#targets.has(event.targetInfo.targetId),
'Target should not exist before targetCreated'
);
this._targets.set(event.targetInfo.targetId, target);
this.#targets.set(event.targetInfo.targetId, target);
if (await target._initializedPromise) {
this.emit(BrowserEmittedEvents.TargetCreated, target);
@ -441,16 +440,16 @@ export class Browser extends EventEmitter {
}
}
private async _targetDestroyed(event: { targetId: string }): Promise<void> {
if (this._ignoredTargets.has(event.targetId)) return;
const target = this._targets.get(event.targetId);
async #targetDestroyed(event: { targetId: string }): Promise<void> {
if (this.#ignoredTargets.has(event.targetId)) return;
const target = this.#targets.get(event.targetId);
if (!target) {
throw new Error(
`Missing target in _targetDestroyed (id = ${event.targetId})`
);
}
target._initializedCallback(false);
this._targets.delete(event.targetId);
this.#targets.delete(event.targetId);
target._closedCallback();
if (await target._initializedPromise) {
this.emit(BrowserEmittedEvents.TargetDestroyed, target);
@ -460,11 +459,9 @@ export class Browser extends EventEmitter {
}
}
private _targetInfoChanged(
event: Protocol.Target.TargetInfoChangedEvent
): void {
if (this._ignoredTargets.has(event.targetInfo.targetId)) return;
const target = this._targets.get(event.targetInfo.targetId);
#targetInfoChanged(event: Protocol.Target.TargetInfoChangedEvent): void {
if (this.#ignoredTargets.has(event.targetInfo.targetId)) return;
const target = this.#targets.get(event.targetInfo.targetId);
if (!target) {
throw new Error(
`Missing target in targetInfoChanged (id = ${event.targetInfo.targetId})`
@ -499,7 +496,7 @@ export class Browser extends EventEmitter {
* | browser endpoint}.
*/
wsEndpoint(): string {
return this._connection.url();
return this.#connection.url();
}
/**
@ -507,19 +504,18 @@ export class Browser extends EventEmitter {
* a default browser context.
*/
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
return this.#defaultContext.newPage();
}
/**
* @internal
* Used by BrowserContext directly so cannot be marked private.
*/
async _createPageInContext(contextId?: string): Promise<Page> {
const { targetId } = await this._connection.send('Target.createTarget', {
const { targetId } = await this.#connection.send('Target.createTarget', {
url: 'about:blank',
browserContextId: contextId || undefined,
});
const target = this._targets.get(targetId);
const target = this.#targets.get(targetId);
if (!target) {
throw new Error(`Missing target for page (id = ${targetId})`);
}
@ -541,7 +537,7 @@ export class Browser extends EventEmitter {
* an array with all the targets in all browser contexts.
*/
targets(): Target[] {
return Array.from(this._targets.values()).filter(
return Array.from(this.#targets.values()).filter(
(target) => target._isInitialized
);
}
@ -628,7 +624,7 @@ export class Browser extends EventEmitter {
* The format of browser.version() might change with future releases of Chromium.
*/
async version(): Promise<string> {
const version = await this._getVersion();
const version = await this.#getVersion();
return version.product;
}
@ -637,7 +633,7 @@ export class Browser extends EventEmitter {
* {@link Page.setUserAgent}.
*/
async userAgent(): Promise<string> {
const version = await this._getVersion();
const version = await this.#getVersion();
return version.userAgent;
}
@ -646,7 +642,7 @@ export class Browser extends EventEmitter {
* itself is considered to be disposed and cannot be used anymore.
*/
async close(): Promise<void> {
await this._closeCallback.call(null);
await this.#closeCallback.call(null);
this.disconnect();
}
@ -656,18 +652,18 @@ export class Browser extends EventEmitter {
* cannot be used anymore.
*/
disconnect(): void {
this._connection.dispose();
this.#connection.dispose();
}
/**
* Indicates that the browser is connected.
*/
isConnected(): boolean {
return !this._connection._closed;
return !this.#connection._closed;
}
private _getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
return this._connection.send('Browser.getVersion');
#getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
return this.#connection.send('Browser.getVersion');
}
}
/**
@ -729,25 +725,25 @@ export const enum BrowserContextEmittedEvents {
* @public
*/
export class BrowserContext extends EventEmitter {
private _connection: Connection;
private _browser: Browser;
private _id?: string;
#connection: Connection;
#browser: Browser;
#id?: string;
/**
* @internal
*/
constructor(connection: Connection, browser: Browser, contextId?: string) {
super();
this._connection = connection;
this._browser = browser;
this._id = contextId;
this.#connection = connection;
this.#browser = browser;
this.#id = contextId;
}
/**
* An array of all active targets inside the browser context.
*/
targets(): Target[] {
return this._browser
return this.#browser
.targets()
.filter((target) => target.browserContext() === this);
}
@ -773,7 +769,7 @@ export class BrowserContext extends EventEmitter {
predicate: (x: Target) => boolean | Promise<boolean>,
options: { timeout?: number } = {}
): Promise<Target> {
return this._browser.waitForTarget(
return this.#browser.waitForTarget(
(target) => target.browserContext() === this && predicate(target),
options
);
@ -793,7 +789,7 @@ export class BrowserContext extends EventEmitter {
(target) =>
target.type() === 'page' ||
(target.type() === 'other' &&
this._browser._getIsPageTargetCallback()?.(
this.#browser._getIsPageTargetCallback()?.(
target._getTargetInfo()
))
)
@ -810,7 +806,7 @@ export class BrowserContext extends EventEmitter {
* The default browser context cannot be closed.
*/
isIncognito(): boolean {
return !!this._id;
return !!this.#id;
}
/**
@ -835,9 +831,9 @@ export class BrowserContext extends EventEmitter {
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {
await this.#connection.send('Browser.grantPermissions', {
origin,
browserContextId: this._id || undefined,
browserContextId: this.#id || undefined,
permissions: protocolPermissions,
});
}
@ -854,8 +850,8 @@ export class BrowserContext extends EventEmitter {
* ```
*/
async clearPermissionOverrides(): Promise<void> {
await this._connection.send('Browser.resetPermissions', {
browserContextId: this._id || undefined,
await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined,
});
}
@ -863,14 +859,14 @@ export class BrowserContext extends EventEmitter {
* Creates a new page in the browser context.
*/
newPage(): Promise<Page> {
return this._browser._createPageInContext(this._id);
return this.#browser._createPageInContext(this.#id);
}
/**
* The browser this browser context belongs to.
*/
browser(): Browser {
return this._browser;
return this.#browser;
}
/**
@ -881,7 +877,7 @@ export class BrowserContext extends EventEmitter {
* Only incognito browser contexts can be closed.
*/
async close(): Promise<void> {
assert(this._id, 'Non-incognito profiles cannot be closed!');
await this._browser._disposeContext(this._id);
assert(this.#id, 'Non-incognito profiles cannot be closed!');
await this.#browser._disposeContext(this.#id);
}
}

View File

@ -54,7 +54,7 @@ export interface BrowserConnectOptions {
/**
* @internal
*/
isPageTarget?: IsPageTargetCallback;
_isPageTarget?: IsPageTargetCallback;
}
const getWebSocketTransportClass = async () => {
@ -67,15 +67,16 @@ const getWebSocketTransportClass = async () => {
/**
* Users should never call this directly; it's called when calling
* `puppeteer.connect`.
*
* @internal
*/
export const connectToBrowser = async (
export async function _connectToBrowser(
options: BrowserConnectOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<Browser> => {
): Promise<Browser> {
const {
browserWSEndpoint,
browserURL,
@ -84,7 +85,7 @@ export const connectToBrowser = async (
transport,
slowMo = 0,
targetFilter,
isPageTarget,
_isPageTarget: isPageTarget,
} = options;
assert(
@ -112,7 +113,7 @@ export const connectToBrowser = async (
const { browserContextIds } = await connection.send(
'Target.getBrowserContexts'
);
return Browser.create(
return Browser._create(
connection,
browserContextIds,
ignoreHTTPSErrors,
@ -122,7 +123,7 @@ export const connectToBrowser = async (
targetFilter,
isPageTarget
);
};
}
async function getWSEndpoint(browserURL: string): Promise<string> {
const endpointURL = new URL('/json/version', browserURL);

View File

@ -27,27 +27,27 @@ export class BrowserWebSocketTransport implements ConnectionTransport {
});
}
private _ws: WebSocket;
#ws: WebSocket;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(ws: WebSocket) {
this._ws = ws;
this._ws.addEventListener('message', (event) => {
this.#ws = ws;
this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', () => {
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', () => {});
this.#ws.addEventListener('error', () => {});
}
send(message: string): void {
this._ws.send(message);
this.#ws.send(message);
}
close(): void {
this._ws.close();
this.#ws.close();
}
}

View File

@ -52,27 +52,33 @@ export const ConnectionEmittedEvents = {
* @public
*/
export class Connection extends EventEmitter {
_url: string;
_transport: ConnectionTransport;
_delay: number;
_lastId = 0;
_sessions: Map<string, CDPSession> = new Map();
_closed = false;
_callbacks: Map<number, ConnectionCallback> = new Map();
#url: string;
#transport: ConnectionTransport;
#delay: number;
#lastId = 0;
#sessions: Map<string, CDPSession> = new Map();
#closed = false;
#callbacks: Map<number, ConnectionCallback> = new Map();
constructor(url: string, transport: ConnectionTransport, delay = 0) {
super();
this._url = url;
this._delay = delay;
this.#url = url;
this.#delay = delay;
this._transport = transport;
this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this);
this.#transport = transport;
this.#transport.onmessage = this.#onMessage.bind(this);
this.#transport.onclose = this.#onClose.bind(this);
}
static fromSession(session: CDPSession): Connection | undefined {
return session._connection;
return session.connection();
}
/**
* @internal
*/
get _closed(): boolean {
return this.#closed;
}
/**
@ -80,11 +86,11 @@ export class Connection extends EventEmitter {
* @returns The current CDP session if it exists
*/
session(sessionId: string): CDPSession | null {
return this._sessions.get(sessionId) || null;
return this.#sessions.get(sessionId) || null;
}
url(): string {
return this._url;
return this.#url;
}
send<T extends keyof ProtocolMapping.Commands>(
@ -100,7 +106,7 @@ export class Connection extends EventEmitter {
const params = paramArgs.length ? paramArgs[0] : undefined;
const id = this._rawSend({ method, params });
return new Promise((resolve, reject) => {
this._callbacks.set(id, {
this.#callbacks.set(id, {
resolve,
reject,
error: new ProtocolError(),
@ -109,18 +115,21 @@ export class Connection extends EventEmitter {
});
}
/**
* @internal
*/
_rawSend(message: Record<string, unknown>): number {
const id = ++this._lastId;
const id = ++this.#lastId;
const stringifiedMessage = JSON.stringify(
Object.assign({}, message, { id })
);
debugProtocolSend(stringifiedMessage);
this._transport.send(stringifiedMessage);
this.#transport.send(stringifiedMessage);
return id;
}
async _onMessage(message: string): Promise<void> {
if (this._delay) await new Promise((f) => setTimeout(f, this._delay));
async #onMessage(message: string): Promise<void> {
if (this.#delay) await new Promise((f) => setTimeout(f, this.#delay));
debugProtocolReceive(message);
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
@ -130,32 +139,32 @@ export class Connection extends EventEmitter {
object.params.targetInfo.type,
sessionId
);
this._sessions.set(sessionId, session);
this.#sessions.set(sessionId, session);
this.emit('sessionattached', session);
const parentSession = this._sessions.get(object.sessionId);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit('sessionattached', session);
}
} else if (object.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
const session = this.#sessions.get(object.params.sessionId);
if (session) {
session._onClosed();
this._sessions.delete(object.params.sessionId);
this.#sessions.delete(object.params.sessionId);
this.emit('sessiondetached', session);
const parentSession = this._sessions.get(object.sessionId);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit('sessiondetached', session);
}
}
}
if (object.sessionId) {
const session = this._sessions.get(object.sessionId);
const session = this.#sessions.get(object.sessionId);
if (session) session._onMessage(object);
} else if (object.id) {
const callback = this._callbacks.get(object.id);
const callback = this.#callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) {
this._callbacks.delete(object.id);
this.#callbacks.delete(object.id);
if (object.error)
callback.reject(
createProtocolError(callback.error, callback.method, object)
@ -167,27 +176,27 @@ export class Connection extends EventEmitter {
}
}
_onClose(): void {
if (this._closed) return;
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
for (const callback of this._callbacks.values())
#onClose(): void {
if (this.#closed) return;
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
for (const callback of this.#callbacks.values())
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Target closed.`
)
);
this._callbacks.clear();
for (const session of this._sessions.values()) session._onClosed();
this._sessions.clear();
this.#callbacks.clear();
for (const session of this.#sessions.values()) session._onClosed();
this.#sessions.clear();
this.emit(ConnectionEmittedEvents.Disconnected);
}
dispose(): void {
this._onClose();
this._transport.close();
this.#onClose();
this.#transport.close();
}
/**
@ -201,7 +210,7 @@ export class Connection extends EventEmitter {
targetId: targetInfo.targetId,
flatten: true,
});
const session = this._sessions.get(sessionId);
const session = this.#sessions.get(sessionId);
if (!session) {
throw new Error('CDPSession creation failed.');
}
@ -255,50 +264,49 @@ export const CDPSessionEmittedEvents = {
* @public
*/
export class CDPSession extends EventEmitter {
/**
* @internal
*/
_connection?: Connection;
private _sessionId: string;
private _targetType: string;
private _callbacks: Map<number, ConnectionCallback> = new Map();
#sessionId: string;
#targetType: string;
#callbacks: Map<number, ConnectionCallback> = new Map();
#connection?: Connection;
/**
* @internal
*/
constructor(connection: Connection, targetType: string, sessionId: string) {
super();
this._connection = connection;
this._targetType = targetType;
this._sessionId = sessionId;
this.#connection = connection;
this.#targetType = targetType;
this.#sessionId = sessionId;
}
connection(): Connection | undefined {
return this._connection;
return this.#connection;
}
send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this._connection)
if (!this.#connection)
return Promise.reject(
new Error(
`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`
`Protocol error (${method}): Session closed. Most likely the ${
this.#targetType
} has been closed.`
)
);
// See the comment in Connection#send explaining why we do this.
const params = paramArgs.length ? paramArgs[0] : undefined;
const id = this._connection._rawSend({
sessionId: this._sessionId,
const id = this.#connection._rawSend({
sessionId: this.#sessionId,
method,
params,
});
return new Promise((resolve, reject) => {
this._callbacks.set(id, {
this.#callbacks.set(id, {
resolve,
reject,
error: new ProtocolError(),
@ -311,9 +319,9 @@ export class CDPSession extends EventEmitter {
* @internal
*/
_onMessage(object: CDPSessionOnMessageObject): void {
const callback = object.id ? this._callbacks.get(object.id) : undefined;
const callback = object.id ? this.#callbacks.get(object.id) : undefined;
if (object.id && callback) {
this._callbacks.delete(object.id);
this.#callbacks.delete(object.id);
if (object.error)
callback.reject(
createProtocolError(callback.error, callback.method, object)
@ -330,12 +338,14 @@ export class CDPSession extends EventEmitter {
* won't emit any events and can't be used to send messages.
*/
async detach(): Promise<void> {
if (!this._connection)
if (!this.#connection)
throw new Error(
`Session already detached. Most likely the ${this._targetType} has been closed.`
`Session already detached. Most likely the ${
this.#targetType
} has been closed.`
);
await this._connection.send('Target.detachFromTarget', {
sessionId: this._sessionId,
await this.#connection.send('Target.detachFromTarget', {
sessionId: this.#sessionId,
});
}
@ -343,23 +353,23 @@ export class CDPSession extends EventEmitter {
* @internal
*/
_onClosed(): void {
for (const callback of this._callbacks.values())
for (const callback of this.#callbacks.values())
callback.reject(
rewriteError(
callback.error,
`Protocol error (${callback.method}): Target closed.`
)
);
this._callbacks.clear();
this._connection = undefined;
this.#callbacks.clear();
this.#connection = undefined;
this.emit(CDPSessionEmittedEvents.Disconnected);
}
/**
* @internal
* Returns the session's id.
*/
id(): string {
return this._sessionId;
return this.#sessionId;
}
}

View File

@ -66,10 +66,10 @@ export type ConsoleMessageType =
* @public
*/
export class ConsoleMessage {
private _type: ConsoleMessageType;
private _text: string;
private _args: JSHandle[];
private _stackTraceLocations: ConsoleMessageLocation[];
#type: ConsoleMessageType;
#text: string;
#args: JSHandle[];
#stackTraceLocations: ConsoleMessageLocation[];
/**
* @public
@ -80,44 +80,44 @@ export class ConsoleMessage {
args: JSHandle[],
stackTraceLocations: ConsoleMessageLocation[]
) {
this._type = type;
this._text = text;
this._args = args;
this._stackTraceLocations = stackTraceLocations;
this.#type = type;
this.#text = text;
this.#args = args;
this.#stackTraceLocations = stackTraceLocations;
}
/**
* @returns The type of the console message.
*/
type(): ConsoleMessageType {
return this._type;
return this.#type;
}
/**
* @returns The text of the console message.
*/
text(): string {
return this._text;
return this.#text;
}
/**
* @returns An array of arguments passed to the console.
*/
args(): JSHandle[] {
return this._args;
return this.#args;
}
/**
* @returns The location of the console message.
*/
location(): ConsoleMessageLocation {
return this._stackTraceLocations[0] ?? {};
return this.#stackTraceLocations[0] ?? {};
}
/**
* @returns The array of locations on the stack of the console message.
*/
stackTrace(): ConsoleMessageLocation[] {
return this._stackTraceLocations;
return this.#stackTraceLocations;
}
}

View File

@ -123,18 +123,12 @@ export interface CSSCoverageOptions {
* @public
*/
export class Coverage {
/**
* @internal
*/
_jsCoverage: JSCoverage;
/**
* @internal
*/
_cssCoverage: CSSCoverage;
#jsCoverage: JSCoverage;
#cssCoverage: CSSCoverage;
constructor(client: CDPSession) {
this._jsCoverage = new JSCoverage(client);
this._cssCoverage = new CSSCoverage(client);
this.#jsCoverage = new JSCoverage(client);
this.#cssCoverage = new CSSCoverage(client);
}
/**
@ -149,7 +143,7 @@ export class Coverage {
* scripts will have `pptr://__puppeteer_evaluation_script__` as their URL.
*/
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
return await this._jsCoverage.start(options);
return await this.#jsCoverage.start(options);
}
/**
@ -161,7 +155,7 @@ export class Coverage {
* However, scripts with sourceURLs are reported.
*/
async stopJSCoverage(): Promise<JSCoverageEntry[]> {
return await this._jsCoverage.stop();
return await this.#jsCoverage.stop();
}
/**
@ -170,7 +164,7 @@ export class Coverage {
* @returns Promise that resolves when coverage is started.
*/
async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> {
return await this._cssCoverage.start(options);
return await this.#cssCoverage.start(options);
}
/**
@ -181,7 +175,7 @@ export class Coverage {
* without sourceURLs.
*/
async stopCSSCoverage(): Promise<CoverageEntry[]> {
return await this._cssCoverage.stop();
return await this.#cssCoverage.stop();
}
}
@ -189,17 +183,17 @@ export class Coverage {
* @public
*/
export class JSCoverage {
_client: CDPSession;
_enabled = false;
_scriptURLs = new Map<string, string>();
_scriptSources = new Map<string, string>();
_eventListeners: PuppeteerEventListener[] = [];
_resetOnNavigation = false;
_reportAnonymousScripts = false;
_includeRawScriptCoverage = false;
#client: CDPSession;
#enabled = false;
#scriptURLs = new Map<string, string>();
#scriptSources = new Map<string, string>();
#eventListeners: PuppeteerEventListener[] = [];
#resetOnNavigation = false;
#reportAnonymousScripts = false;
#includeRawScriptCoverage = false;
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
async start(
@ -209,60 +203,60 @@ export class JSCoverage {
includeRawScriptCoverage?: boolean;
} = {}
): Promise<void> {
assert(!this._enabled, 'JSCoverage is already enabled');
assert(!this.#enabled, 'JSCoverage is already enabled');
const {
resetOnNavigation = true,
reportAnonymousScripts = false,
includeRawScriptCoverage = false,
} = options;
this._resetOnNavigation = resetOnNavigation;
this._reportAnonymousScripts = reportAnonymousScripts;
this._includeRawScriptCoverage = includeRawScriptCoverage;
this._enabled = true;
this._scriptURLs.clear();
this._scriptSources.clear();
this._eventListeners = [
this.#resetOnNavigation = resetOnNavigation;
this.#reportAnonymousScripts = reportAnonymousScripts;
this.#includeRawScriptCoverage = includeRawScriptCoverage;
this.#enabled = true;
this.#scriptURLs.clear();
this.#scriptSources.clear();
this.#eventListeners = [
helper.addEventListener(
this._client,
this.#client,
'Debugger.scriptParsed',
this._onScriptParsed.bind(this)
this.#onScriptParsed.bind(this)
),
helper.addEventListener(
this._client,
this.#client,
'Runtime.executionContextsCleared',
this._onExecutionContextsCleared.bind(this)
this.#onExecutionContextsCleared.bind(this)
),
];
await Promise.all([
this._client.send('Profiler.enable'),
this._client.send('Profiler.startPreciseCoverage', {
callCount: this._includeRawScriptCoverage,
this.#client.send('Profiler.enable'),
this.#client.send('Profiler.startPreciseCoverage', {
callCount: this.#includeRawScriptCoverage,
detailed: true,
}),
this._client.send('Debugger.enable'),
this._client.send('Debugger.setSkipAllPauses', { skip: true }),
this.#client.send('Debugger.enable'),
this.#client.send('Debugger.setSkipAllPauses', { skip: true }),
]);
}
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation) return;
this._scriptURLs.clear();
this._scriptSources.clear();
#onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) return;
this.#scriptURLs.clear();
this.#scriptSources.clear();
}
async _onScriptParsed(
async #onScriptParsed(
event: Protocol.Debugger.ScriptParsedEvent
): Promise<void> {
// Ignore puppeteer-injected scripts
if (event.url === EVALUATION_SCRIPT_URL) return;
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts) return;
if (!event.url && !this.#reportAnonymousScripts) return;
try {
const response = await this._client.send('Debugger.getScriptSource', {
const response = await this.#client.send('Debugger.getScriptSource', {
scriptId: event.scriptId,
});
this._scriptURLs.set(event.scriptId, event.url);
this._scriptSources.set(event.scriptId, response.scriptSource);
this.#scriptURLs.set(event.scriptId, event.url);
this.#scriptSources.set(event.scriptId, response.scriptSource);
} catch (error) {
// This might happen if the page has already navigated away.
debugError(error);
@ -270,31 +264,31 @@ export class JSCoverage {
}
async stop(): Promise<JSCoverageEntry[]> {
assert(this._enabled, 'JSCoverage is not enabled');
this._enabled = false;
assert(this.#enabled, 'JSCoverage is not enabled');
this.#enabled = false;
const result = await Promise.all([
this._client.send('Profiler.takePreciseCoverage'),
this._client.send('Profiler.stopPreciseCoverage'),
this._client.send('Profiler.disable'),
this._client.send('Debugger.disable'),
this.#client.send('Profiler.takePreciseCoverage'),
this.#client.send('Profiler.stopPreciseCoverage'),
this.#client.send('Profiler.disable'),
this.#client.send('Debugger.disable'),
]);
helper.removeEventListeners(this._eventListeners);
helper.removeEventListeners(this.#eventListeners);
const coverage = [];
const profileResponse = result[0];
for (const entry of profileResponse.result) {
let url = this._scriptURLs.get(entry.scriptId);
if (!url && this._reportAnonymousScripts)
let url = this.#scriptURLs.get(entry.scriptId);
if (!url && this.#reportAnonymousScripts)
url = 'debugger://VM' + entry.scriptId;
const text = this._scriptSources.get(entry.scriptId);
const text = this.#scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined) continue;
const flattenRanges = [];
for (const func of entry.functions) flattenRanges.push(...func.ranges);
const ranges = convertToDisjointRanges(flattenRanges);
if (!this._includeRawScriptCoverage) {
if (!this.#includeRawScriptCoverage) {
coverage.push({ url, ranges, text });
} else {
coverage.push({ url, ranges, text, rawScriptCoverage: entry });
@ -308,60 +302,59 @@ export class JSCoverage {
* @public
*/
export class CSSCoverage {
_client: CDPSession;
_enabled = false;
_stylesheetURLs = new Map<string, string>();
_stylesheetSources = new Map<string, string>();
_eventListeners: PuppeteerEventListener[] = [];
_resetOnNavigation = false;
_reportAnonymousScripts = false;
#client: CDPSession;
#enabled = false;
#stylesheetURLs = new Map<string, string>();
#stylesheetSources = new Map<string, string>();
#eventListeners: PuppeteerEventListener[] = [];
#resetOnNavigation = false;
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
async start(options: { resetOnNavigation?: boolean } = {}): Promise<void> {
assert(!this._enabled, 'CSSCoverage is already enabled');
assert(!this.#enabled, 'CSSCoverage is already enabled');
const { resetOnNavigation = true } = options;
this._resetOnNavigation = resetOnNavigation;
this._enabled = true;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
this._eventListeners = [
this.#resetOnNavigation = resetOnNavigation;
this.#enabled = true;
this.#stylesheetURLs.clear();
this.#stylesheetSources.clear();
this.#eventListeners = [
helper.addEventListener(
this._client,
this.#client,
'CSS.styleSheetAdded',
this._onStyleSheet.bind(this)
this.#onStyleSheet.bind(this)
),
helper.addEventListener(
this._client,
this.#client,
'Runtime.executionContextsCleared',
this._onExecutionContextsCleared.bind(this)
this.#onExecutionContextsCleared.bind(this)
),
];
await Promise.all([
this._client.send('DOM.enable'),
this._client.send('CSS.enable'),
this._client.send('CSS.startRuleUsageTracking'),
this.#client.send('DOM.enable'),
this.#client.send('CSS.enable'),
this.#client.send('CSS.startRuleUsageTracking'),
]);
}
_onExecutionContextsCleared(): void {
if (!this._resetOnNavigation) return;
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
#onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) return;
this.#stylesheetURLs.clear();
this.#stylesheetSources.clear();
}
async _onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL) return;
try {
const response = await this._client.send('CSS.getStyleSheetText', {
const response = await this.#client.send('CSS.getStyleSheetText', {
styleSheetId: header.styleSheetId,
});
this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
this._stylesheetSources.set(header.styleSheetId, response.text);
this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
this.#stylesheetSources.set(header.styleSheetId, response.text);
} catch (error) {
// This might happen if the page has already navigated away.
debugError(error);
@ -369,16 +362,16 @@ export class CSSCoverage {
}
async stop(): Promise<CoverageEntry[]> {
assert(this._enabled, 'CSSCoverage is not enabled');
this._enabled = false;
const ruleTrackingResponse = await this._client.send(
assert(this.#enabled, 'CSSCoverage is not enabled');
this.#enabled = false;
const ruleTrackingResponse = await this.#client.send(
'CSS.stopRuleUsageTracking'
);
await Promise.all([
this._client.send('CSS.disable'),
this._client.send('DOM.disable'),
this.#client.send('CSS.disable'),
this.#client.send('DOM.disable'),
]);
helper.removeEventListeners(this._eventListeners);
helper.removeEventListeners(this.#eventListeners);
// aggregate by styleSheetId
const styleSheetIdToCoverage = new Map();
@ -396,10 +389,10 @@ export class CSSCoverage {
}
const coverage: CoverageEntry[] = [];
for (const styleSheetId of this._stylesheetURLs.keys()) {
const url = this._stylesheetURLs.get(styleSheetId);
for (const styleSheetId of this.#stylesheetURLs.keys()) {
const url = this.#stylesheetURLs.get(styleSheetId);
assert(url);
const text = this._stylesheetSources.get(styleSheetId);
const text = this.#stylesheetSources.get(styleSheetId);
assert(text);
const ranges = convertToDisjointRanges(
styleSheetIdToCoverage.get(styleSheetId) || []

View File

@ -35,7 +35,7 @@ import {
LifecycleWatcher,
PuppeteerLifeCycleEvent,
} from './LifecycleWatcher.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { _getQueryHandlerAndSelector } from './QueryHandler.js';
import { TimeoutSettings } from './TimeoutSettings.js';
// predicateQueryHandler and checkWaitForOptions are declared here so that
@ -72,30 +72,37 @@ export interface PageBinding {
* @internal
*/
export class DOMWorld {
private _frameManager: FrameManager;
private _client: CDPSession;
private _frame: Frame;
private _timeoutSettings: TimeoutSettings;
private _documentPromise: Promise<ElementHandle> | null = null;
private _contextPromise: Promise<ExecutionContext> | null = null;
#frameManager: FrameManager;
#client: CDPSession;
#frame: Frame;
#timeoutSettings: TimeoutSettings;
#documentPromise: Promise<ElementHandle> | null = null;
#contextPromise: Promise<ExecutionContext> | null = null;
#contextResolveCallback: ((x: ExecutionContext) => void) | null = null;
#detached = false;
private _contextResolveCallback: ((x: ExecutionContext) => void) | null =
null;
private _detached = false;
/**
* @internal
*/
_waitTasks = new Set<WaitTask>();
/**
* @internal
* Contains mapping from functions that should be bound to Puppeteer functions.
*/
_boundFunctions = new Map<string, Function>();
// Set of bindings that have been registered in the current context.
private _ctxBindings = new Set<string>();
private static bindingIdentifier = (name: string, contextId: number) =>
#ctxBindings = new Set<string>();
// Contains mapping from functions that should be bound to Puppeteer functions.
#boundFunctions = new Map<string, Function>();
#waitTasks = new Set<WaitTask>();
/**
* @internal
*/
get _waitTasks(): Set<WaitTask> {
return this.#waitTasks;
}
/**
* @internal
*/
get _boundFunctions(): Map<string, Function> {
return this.#boundFunctions;
}
static #bindingIdentifier = (name: string, contextId: number) =>
`${name}_${contextId}`;
constructor(
@ -106,44 +113,52 @@ export class DOMWorld {
) {
// Keep own reference to client because it might differ from the FrameManager's
// client for OOP iframes.
this._client = client;
this._frameManager = frameManager;
this._frame = frame;
this._timeoutSettings = timeoutSettings;
this.#client = client;
this.#frameManager = frameManager;
this.#frame = frame;
this.#timeoutSettings = timeoutSettings;
this._setContext(null);
this._onBindingCalled = this._onBindingCalled.bind(this);
this._client.on('Runtime.bindingCalled', this._onBindingCalled);
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
}
frame(): Frame {
return this._frame;
return this.#frame;
}
/**
* @internal
*/
async _setContext(context: ExecutionContext | null): Promise<void> {
if (context) {
assert(
this._contextResolveCallback,
this.#contextResolveCallback,
'Execution Context has already been set.'
);
this._ctxBindings.clear();
this._contextResolveCallback?.call(null, context);
this._contextResolveCallback = null;
this.#ctxBindings.clear();
this.#contextResolveCallback?.call(null, context);
this.#contextResolveCallback = null;
for (const waitTask of this._waitTasks) waitTask.rerun();
} else {
this._documentPromise = null;
this._contextPromise = new Promise((fulfill) => {
this._contextResolveCallback = fulfill;
this.#documentPromise = null;
this.#contextPromise = new Promise((fulfill) => {
this.#contextResolveCallback = fulfill;
});
}
}
/**
* @internal
*/
_hasContext(): boolean {
return !this._contextResolveCallback;
return !this.#contextResolveCallback;
}
/**
* @internal
*/
_detach(): void {
this._detached = true;
this._client.off('Runtime.bindingCalled', this._onBindingCalled);
this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
for (const waitTask of this._waitTasks)
waitTask.terminate(
new Error('waitForFunction failed: frame got detached.')
@ -151,13 +166,13 @@ export class DOMWorld {
}
executionContext(): Promise<ExecutionContext> {
if (this._detached)
if (this.#detached)
throw new Error(
`Execution context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`
);
if (this._contextPromise === null)
if (this.#contextPromise === null)
throw new Error(`Execution content promise is missing`);
return this._contextPromise;
return this.#contextPromise;
}
async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
@ -187,9 +202,12 @@ export class DOMWorld {
return value;
}
/**
* @internal
*/
async _document(): Promise<ElementHandle> {
if (this._documentPromise) return this._documentPromise;
this._documentPromise = this.executionContext().then(async (context) => {
if (this.#documentPromise) return this.#documentPromise;
this.#documentPromise = this.executionContext().then(async (context) => {
const document = await context.evaluateHandle('document');
const element = document.asElement();
if (element === null) {
@ -197,7 +215,7 @@ export class DOMWorld {
}
return element;
});
return this._documentPromise;
return this.#documentPromise;
}
async $x(expression: string): Promise<ElementHandle[]> {
@ -263,7 +281,7 @@ export class DOMWorld {
): Promise<void> {
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this.#timeoutSettings.navigationTimeout(),
} = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
@ -273,8 +291,8 @@ export class DOMWorld {
document.close();
}, html);
const watcher = new LifecycleWatcher(
this._frameManager,
this._frame,
this.#frameManager,
this.#frame,
waitUntil,
timeout
);
@ -560,33 +578,34 @@ export class DOMWorld {
options: WaitForSelectorOptions
): Promise<ElementHandle | null> {
const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector);
_getQueryHandlerAndSelector(selector);
assert(queryHandler.waitFor, 'Query handler does not support waiting');
return queryHandler.waitFor(this, updatedSelector, options);
}
// If multiple waitFor are set up asynchronously, we need to wait for the
// first one to set up the binding in the page before running the others.
private _settingUpBinding: Promise<void> | null = null;
#settingUpBinding: Promise<void> | null = null;
/**
* @internal
*/
async addBindingToContext(
async _addBindingToContext(
context: ExecutionContext,
name: string
): Promise<void> {
// Previous operation added the binding so we are done.
if (
this._ctxBindings.has(
DOMWorld.bindingIdentifier(name, context._contextId)
this.#ctxBindings.has(
DOMWorld.#bindingIdentifier(name, context._contextId)
)
) {
return;
}
// Wait for other operation to finish
if (this._settingUpBinding) {
await this._settingUpBinding;
return this.addBindingToContext(context, name);
if (this.#settingUpBinding) {
await this.#settingUpBinding;
return this._addBindingToContext(context, name);
}
const bind = async (name: string) => {
@ -617,19 +636,19 @@ export class DOMWorld {
return;
}
}
this._ctxBindings.add(
DOMWorld.bindingIdentifier(name, context._contextId)
this.#ctxBindings.add(
DOMWorld.#bindingIdentifier(name, context._contextId)
);
};
this._settingUpBinding = bind(name);
await this._settingUpBinding;
this._settingUpBinding = null;
this.#settingUpBinding = bind(name);
await this.#settingUpBinding;
this.#settingUpBinding = null;
}
private async _onBindingCalled(
#onBindingCalled = async (
event: Protocol.Runtime.BindingCalledEvent
): Promise<void> {
): Promise<void> => {
let payload: { type: string; name: string; seq: number; args: unknown[] };
if (!this._hasContext()) return;
const context = await this.executionContext();
@ -643,8 +662,8 @@ export class DOMWorld {
const { type, name, seq, args } = payload;
if (
type !== 'internal' ||
!this._ctxBindings.has(
DOMWorld.bindingIdentifier(name, context._contextId)
!this.#ctxBindings.has(
DOMWorld.#bindingIdentifier(name, context._contextId)
)
)
return;
@ -673,12 +692,12 @@ export class DOMWorld {
// @ts-ignore Code is evaluated in a different context.
globalThis[name].callbacks.delete(seq);
}
}
};
/**
* @internal
*/
async waitForSelectorInPage(
async _waitForSelectorInPage(
queryOne: Function,
selector: string,
options: WaitForSelectorOptions,
@ -687,7 +706,7 @@ export class DOMWorld {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this._timeoutSettings.timeout(),
timeout = this.#timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `selector \`${selector}\`${
@ -732,7 +751,7 @@ export class DOMWorld {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this._timeoutSettings.timeout(),
timeout = this.#timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`;
@ -776,7 +795,7 @@ export class DOMWorld {
options: { polling?: string | number; timeout?: number } = {},
...args: SerializableOrJSHandle[]
): Promise<JSHandle> {
const { polling = 'raf', timeout = this._timeoutSettings.timeout() } =
const { polling = 'raf', timeout = this.#timeoutSettings.timeout() } =
options;
const waitTaskOptions: WaitTaskOptions = {
domWorld: this,
@ -817,20 +836,21 @@ const noop = (): void => {};
* @internal
*/
export class WaitTask {
_domWorld: DOMWorld;
_polling: string | number;
_timeout: number;
_predicateBody: string;
_predicateAcceptsContextElement: boolean;
_args: SerializableOrJSHandle[];
_binding?: PageBinding;
_runCount = 0;
#domWorld: DOMWorld;
#polling: string | number;
#timeout: number;
#predicateBody: string;
#predicateAcceptsContextElement: boolean;
#args: SerializableOrJSHandle[];
#binding?: PageBinding;
#runCount = 0;
#resolve: (x: JSHandle) => void = noop;
#reject: (x: Error) => void = noop;
#timeoutTimer?: NodeJS.Timeout;
#terminated = false;
#root: ElementHandle | null = null;
promise: Promise<JSHandle>;
_resolve: (x: JSHandle) => void = noop;
_reject: (x: Error) => void = noop;
_timeoutTimer?: NodeJS.Timeout;
_terminated = false;
_root: ElementHandle | null = null;
constructor(options: WaitTaskOptions) {
if (helper.isString(options.polling))
@ -850,26 +870,26 @@ export class WaitTask {
return `return (${predicateBody})(...args);`;
}
this._domWorld = options.domWorld;
this._polling = options.polling;
this._timeout = options.timeout;
this._root = options.root || null;
this._predicateBody = getPredicateBody(options.predicateBody);
this._predicateAcceptsContextElement =
this.#domWorld = options.domWorld;
this.#polling = options.polling;
this.#timeout = options.timeout;
this.#root = options.root || null;
this.#predicateBody = getPredicateBody(options.predicateBody);
this.#predicateAcceptsContextElement =
options.predicateAcceptsContextElement;
this._args = options.args;
this._binding = options.binding;
this._runCount = 0;
this._domWorld._waitTasks.add(this);
if (this._binding) {
this._domWorld._boundFunctions.set(
this._binding.name,
this._binding.pptrFunction
this.#args = options.args;
this.#binding = options.binding;
this.#runCount = 0;
this.#domWorld._waitTasks.add(this);
if (this.#binding) {
this.#domWorld._boundFunctions.set(
this.#binding.name,
this.#binding.pptrFunction
);
}
this.promise = new Promise<JSHandle>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
this.#resolve = resolve;
this.#reject = reject;
});
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
@ -877,7 +897,7 @@ export class WaitTask {
const timeoutError = new TimeoutError(
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
);
this._timeoutTimer = setTimeout(
this.#timeoutTimer = setTimeout(
() => this.terminate(timeoutError),
options.timeout
);
@ -886,36 +906,36 @@ export class WaitTask {
}
terminate(error: Error): void {
this._terminated = true;
this._reject(error);
this._cleanup();
this.#terminated = true;
this.#reject(error);
this.#cleanup();
}
async rerun(): Promise<void> {
const runCount = ++this._runCount;
const runCount = ++this.#runCount;
let success: JSHandle | null = null;
let error: Error | null = null;
const context = await this._domWorld.executionContext();
if (this._terminated || runCount !== this._runCount) return;
if (this._binding) {
await this._domWorld.addBindingToContext(context, this._binding.name);
const context = await this.#domWorld.executionContext();
if (this.#terminated || runCount !== this.#runCount) return;
if (this.#binding) {
await this.#domWorld._addBindingToContext(context, this.#binding.name);
}
if (this._terminated || runCount !== this._runCount) return;
if (this.#terminated || runCount !== this.#runCount) return;
try {
success = await context.evaluateHandle(
waitForPredicatePageFunction,
this._root || null,
this._predicateBody,
this._predicateAcceptsContextElement,
this._polling,
this._timeout,
...this._args
this.#root || null,
this.#predicateBody,
this.#predicateAcceptsContextElement,
this.#polling,
this.#timeout,
...this.#args
);
} catch (error_) {
error = error_ as Error;
}
if (this._terminated || runCount !== this._runCount) {
if (this.#terminated || runCount !== this.#runCount) {
if (success) await success.dispose();
return;
}
@ -925,7 +945,7 @@ export class WaitTask {
// throw an error - ignore this predicate run altogether.
if (
!error &&
(await this._domWorld.evaluate((s) => !s, success).catch(() => true))
(await this.#domWorld.evaluate((s) => !s, success).catch(() => true))
) {
if (!success)
throw new Error('Assertion: result handle is not available');
@ -959,18 +979,18 @@ export class WaitTask {
if (error.message.includes('Cannot find context with specified id'))
return;
this._reject(error);
this.#reject(error);
} else {
if (!success)
throw new Error('Assertion: result handle is not available');
this._resolve(success);
this.#resolve(success);
}
this._cleanup();
this.#cleanup();
}
_cleanup(): void {
this._timeoutTimer !== undefined && clearTimeout(this._timeoutTimer);
this._domWorld._waitTasks.delete(this);
#cleanup(): void {
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
this.#domWorld._waitTasks.delete(this);
}
}

View File

@ -1537,6 +1537,8 @@ export type DevicesMap = {
/**
* @internal
*/
export const devicesMap: DevicesMap = {};
export const _devicesMap: DevicesMap = {};
for (const device of devices) devicesMap[device.name] = device;
for (const device of devices) {
_devicesMap[device.name] = device;
}

View File

@ -41,11 +41,11 @@ import { Protocol } from 'devtools-protocol';
* @public
*/
export class Dialog {
private _client: CDPSession;
private _type: Protocol.Page.DialogType;
private _message: string;
private _defaultValue: string;
private _handled = false;
#client: CDPSession;
#type: Protocol.Page.DialogType;
#message: string;
#defaultValue: string;
#handled = false;
/**
* @internal
@ -56,24 +56,24 @@ export class Dialog {
message: string,
defaultValue = ''
) {
this._client = client;
this._type = type;
this._message = message;
this._defaultValue = defaultValue;
this.#client = client;
this.#type = type;
this.#message = message;
this.#defaultValue = defaultValue;
}
/**
* @returns The type of the dialog.
*/
type(): Protocol.Page.DialogType {
return this._type;
return this.#type;
}
/**
* @returns The message displayed in the dialog.
*/
message(): string {
return this._message;
return this.#message;
}
/**
@ -81,7 +81,7 @@ export class Dialog {
* is not a `prompt`.
*/
defaultValue(): string {
return this._defaultValue;
return this.#defaultValue;
}
/**
@ -91,9 +91,9 @@ export class Dialog {
* @returns A promise that resolves when the dialog has been accepted.
*/
async accept(promptText?: string): Promise<void> {
assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
assert(!this.#handled, 'Cannot accept dialog which is already handled!');
this.#handled = true;
await this.#client.send('Page.handleJavaScriptDialog', {
accept: true,
promptText: promptText,
});
@ -103,9 +103,9 @@ export class Dialog {
* @returns A promise which will resolve once the dialog has been dismissed
*/
async dismiss(): Promise<void> {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
await this._client.send('Page.handleJavaScriptDialog', {
assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
this.#handled = true;
await this.#client.send('Page.handleJavaScriptDialog', {
accept: false,
});
}

View File

@ -18,12 +18,12 @@ import { Viewport } from './PuppeteerViewport.js';
import { Protocol } from 'devtools-protocol';
export class EmulationManager {
_client: CDPSession;
_emulatingMobile = false;
_hasTouch = false;
#client: CDPSession;
#emulatingMobile = false;
#hasTouch = false;
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
async emulateViewport(viewport: Viewport): Promise<boolean> {
@ -38,22 +38,22 @@ export class EmulationManager {
const hasTouch = viewport.hasTouch || false;
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', {
this.#client.send('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation,
}),
this._client.send('Emulation.setTouchEmulationEnabled', {
this.#client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch,
}),
]);
const reloadNeeded =
this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
this._emulatingMobile = mobile;
this._hasTouch = hasTouch;
this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
this.#emulatingMobile = mobile;
this.#hasTouch = hasTouch;
return reloadNeeded;
}
}

View File

@ -16,7 +16,7 @@
import { assert } from './assert.js';
import { helper } from './helper.js';
import { createJSHandle, JSHandle, ElementHandle } from './JSHandle.js';
import { _createJSHandle, JSHandle, ElementHandle } from './JSHandle.js';
import { CDPSession } from './Connection.js';
import { DOMWorld } from './DOMWorld.js';
import { Frame } from './FrameManager.js';
@ -137,11 +137,7 @@ export class ExecutionContext {
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this._evaluateInternal<ReturnType>(
true,
pageFunction,
...args
);
return await this.#evaluate<ReturnType>(true, pageFunction, ...args);
}
/**
@ -190,10 +186,10 @@ export class ExecutionContext {
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandleType> {
return this._evaluateInternal<HandleType>(false, pageFunction, ...args);
return this.#evaluate<HandleType>(false, pageFunction, ...args);
}
private async _evaluateInternal<ReturnType>(
async #evaluate<ReturnType>(
returnByValue: boolean,
pageFunction: Function | string,
...args: unknown[]
@ -224,7 +220,7 @@ export class ExecutionContext {
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
: _createJSHandle(this, remoteObject);
}
if (typeof pageFunction !== 'function')
@ -263,8 +259,9 @@ export class ExecutionContext {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
)
error.message += ' Are you passing a nested JSHandle?';
) {
error.message += ' Recursive objects are not allowed.';
}
throw error;
}
const { exceptionDetails, result: remoteObject } =
@ -275,7 +272,7 @@ export class ExecutionContext {
);
return returnByValue
? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
: _createJSHandle(this, remoteObject);
function convertArgument(
this: ExecutionContext,
@ -355,7 +352,7 @@ export class ExecutionContext {
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId,
});
return createJSHandle(this, response.objects);
return _createJSHandle(this, response.objects);
}
/**
@ -368,7 +365,7 @@ export class ExecutionContext {
backendNodeId: backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle;
return _createJSHandle(this, object) as ElementHandle;
}
/**

View File

@ -37,9 +37,9 @@ import { assert } from './assert.js';
* @public
*/
export class FileChooser {
private _element: ElementHandle;
private _multiple: boolean;
private _handled = false;
#element: ElementHandle;
#multiple: boolean;
#handled = false;
/**
* @internal
@ -48,15 +48,15 @@ export class FileChooser {
element: ElementHandle,
event: Protocol.Page.FileChooserOpenedEvent
) {
this._element = element;
this._multiple = event.mode !== 'selectSingle';
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;
return this.#multiple;
}
/**
@ -66,11 +66,11 @@ export class FileChooser {
*/
async accept(filePaths: string[]): Promise<void> {
assert(
!this._handled,
!this.#handled,
'Cannot accept FileChooser which is already handled!'
);
this._handled = true;
await this._element.uploadFile(...filePaths);
this.#handled = true;
await this.#element.uploadFile(...filePaths);
}
/**
@ -78,9 +78,9 @@ export class FileChooser {
*/
cancel(): void {
assert(
!this._handled,
!this.#handled,
'Cannot cancel FileChooser which is already handled!'
);
this._handled = true;
this.#handled = true;
}
}

View File

@ -66,14 +66,28 @@ export const FrameManagerEmittedEvents = {
* @internal
*/
export class FrameManager extends EventEmitter {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
_timeoutSettings: TimeoutSettings;
private _frames = new Map<string, Frame>();
private _contextIdToContext = new Map<string, ExecutionContext>();
private _isolatedWorlds = new Set<string>();
private _mainFrame?: Frame;
#page: Page;
#networkManager: NetworkManager;
#timeoutSettings: TimeoutSettings;
#frames = new Map<string, Frame>();
#contextIdToContext = new Map<string, ExecutionContext>();
#isolatedWorlds = new Set<string>();
#mainFrame?: Frame;
#client: CDPSession;
/**
* @internal
*/
get _timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings;
}
/**
* @internal
*/
get _client(): CDPSession {
return this.#client;
}
constructor(
client: CDPSession,
@ -82,64 +96,64 @@ export class FrameManager extends EventEmitter {
timeoutSettings: TimeoutSettings
) {
super();
this._client = client;
this._page = page;
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this.setupEventListeners(this._client);
this.#client = client;
this.#page = page;
this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this.#timeoutSettings = timeoutSettings;
this.setupEventListeners(this.#client);
}
private setupEventListeners(session: CDPSession) {
session.on('Page.frameAttached', (event) => {
this._onFrameAttached(session, event.frameId, event.parentFrameId);
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
});
session.on('Page.frameNavigated', (event) => {
this._onFrameNavigated(event.frame);
this.#onFrameNavigated(event.frame);
});
session.on('Page.navigatedWithinDocument', (event) => {
this._onFrameNavigatedWithinDocument(event.frameId, event.url);
this.#onFrameNavigatedWithinDocument(event.frameId, event.url);
});
session.on(
'Page.frameDetached',
(event: Protocol.Page.FrameDetachedEvent) => {
this._onFrameDetached(
this.#onFrameDetached(
event.frameId,
event.reason as Protocol.Page.FrameDetachedEventReason
);
}
);
session.on('Page.frameStartedLoading', (event) => {
this._onFrameStartedLoading(event.frameId);
this.#onFrameStartedLoading(event.frameId);
});
session.on('Page.frameStoppedLoading', (event) => {
this._onFrameStoppedLoading(event.frameId);
this.#onFrameStoppedLoading(event.frameId);
});
session.on('Runtime.executionContextCreated', (event) => {
this._onExecutionContextCreated(event.context, session);
this.#onExecutionContextCreated(event.context, session);
});
session.on('Runtime.executionContextDestroyed', (event) => {
this._onExecutionContextDestroyed(event.executionContextId, session);
this.#onExecutionContextDestroyed(event.executionContextId, session);
});
session.on('Runtime.executionContextsCleared', () => {
this._onExecutionContextsCleared(session);
this.#onExecutionContextsCleared(session);
});
session.on('Page.lifecycleEvent', (event) => {
this._onLifecycleEvent(event);
this.#onLifecycleEvent(event);
});
session.on('Target.attachedToTarget', async (event) => {
this._onAttachedToTarget(event);
this.#onAttachedToTarget(event);
});
session.on('Target.detachedFromTarget', async (event) => {
this._onDetachedFromTarget(event);
this.#onDetachedFromTarget(event);
});
}
async initialize(client: CDPSession = this._client): Promise<void> {
async initialize(client: CDPSession = this.#client): Promise<void> {
try {
const result = await Promise.all([
client.send('Page.enable'),
client.send('Page.getFrameTree'),
client !== this._client
client !== this.#client
? client.send('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: false,
@ -149,15 +163,15 @@ export class FrameManager extends EventEmitter {
]);
const { frameTree } = result[1];
this._handleFrameTree(client, frameTree);
this.#handleFrameTree(client, frameTree);
await Promise.all([
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
client
.send('Runtime.enable')
.then(() => this._ensureIsolatedWorld(client, UTILITY_WORLD_NAME)),
// TODO: Network manager is not aware of OOP iframes yet.
client === this._client
? this._networkManager.initialize()
client === this.#client
? this.#networkManager.initialize()
: Promise.resolve(),
]);
} catch (error) {
@ -175,7 +189,7 @@ export class FrameManager extends EventEmitter {
}
networkManager(): NetworkManager {
return this._networkManager;
return this.#networkManager;
}
async navigateFrame(
@ -189,14 +203,14 @@ export class FrameManager extends EventEmitter {
): Promise<HTTPResponse | null> {
assertNoLegacyNavigationOptions(options);
const {
referer = this._networkManager.extraHTTPHeaders()['referer'],
referer = this.#networkManager.extraHTTPHeaders()['referer'],
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this.#timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
let error = await Promise.race([
navigate(this._client, url, referer, frame._id),
navigate(this.#client, url, referer, frame._id),
watcher.timeoutOrTerminationPromise(),
]);
if (!error) {
@ -244,7 +258,7 @@ export class FrameManager extends EventEmitter {
assertNoLegacyNavigationOptions(options);
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this.#timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([
@ -257,15 +271,13 @@ export class FrameManager extends EventEmitter {
return await watcher.navigationResponse();
}
private async _onAttachedToTarget(
event: Protocol.Target.AttachedToTargetEvent
) {
async #onAttachedToTarget(event: Protocol.Target.AttachedToTargetEvent) {
if (event.targetInfo.type !== 'iframe') {
return;
}
const frame = this._frames.get(event.targetInfo.targetId);
const connection = Connection.fromSession(this._client);
const frame = this.#frames.get(event.targetInfo.targetId);
const connection = Connection.fromSession(this.#client);
assert(connection);
const session = connection.session(event.sessionId);
assert(session);
@ -274,81 +286,79 @@ export class FrameManager extends EventEmitter {
await this.initialize(session);
}
private async _onDetachedFromTarget(
event: Protocol.Target.DetachedFromTargetEvent
) {
async #onDetachedFromTarget(event: Protocol.Target.DetachedFromTargetEvent) {
if (!event.targetId) return;
const frame = this._frames.get(event.targetId);
const frame = this.#frames.get(event.targetId);
if (frame && frame.isOOPFrame()) {
// When an OOP iframe is removed from the page, it
// will only get a Target.detachedFromTarget event.
this._removeFramesRecursively(frame);
this.#removeFramesRecursively(frame);
}
}
_onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this._frames.get(event.frameId);
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.#frames.get(event.frameId);
if (!frame) return;
frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
}
_onFrameStartedLoading(frameId: string): void {
const frame = this._frames.get(frameId);
#onFrameStartedLoading(frameId: string): void {
const frame = this.#frames.get(frameId);
if (!frame) return;
frame._onLoadingStarted();
}
_onFrameStoppedLoading(frameId: string): void {
const frame = this._frames.get(frameId);
#onFrameStoppedLoading(frameId: string): void {
const frame = this.#frames.get(frameId);
if (!frame) return;
frame._onLoadingStopped();
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
}
_handleFrameTree(
#handleFrameTree(
session: CDPSession,
frameTree: Protocol.Page.FrameTree
): void {
if (frameTree.frame.parentId) {
this._onFrameAttached(
this.#onFrameAttached(
session,
frameTree.frame.id,
frameTree.frame.parentId
);
}
this._onFrameNavigated(frameTree.frame);
this.#onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames) return;
for (const child of frameTree.childFrames) {
this._handleFrameTree(session, child);
this.#handleFrameTree(session, child);
}
}
page(): Page {
return this._page;
return this.#page;
}
mainFrame(): Frame {
assert(this._mainFrame, 'Requesting main frame too early!');
return this._mainFrame;
assert(this.#mainFrame, 'Requesting main frame too early!');
return this.#mainFrame;
}
frames(): Frame[] {
return Array.from(this._frames.values());
return Array.from(this.#frames.values());
}
frame(frameId: string): Frame | null {
return this._frames.get(frameId) || null;
return this.#frames.get(frameId) || null;
}
_onFrameAttached(
#onFrameAttached(
session: CDPSession,
frameId: string,
parentFrameId?: string
): void {
if (this._frames.has(frameId)) {
const frame = this._frames.get(frameId)!;
if (this.#frames.has(frameId)) {
const frame = this.#frames.get(frameId)!;
if (session && frame.isOOPFrame()) {
// If an OOP iframes becomes a normal iframe again
// it is first attached to the parent page before
@ -358,18 +368,18 @@ export class FrameManager extends EventEmitter {
return;
}
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const parentFrame = this.#frames.get(parentFrameId);
assert(parentFrame);
const frame = new Frame(this, parentFrame, frameId, session);
this._frames.set(frame._id, frame);
this.#frames.set(frame._id, frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
}
_onFrameNavigated(framePayload: Protocol.Page.Frame): void {
#onFrameNavigated(framePayload: Protocol.Page.Frame): void {
const isMainFrame = !framePayload.parentId;
let frame = isMainFrame
? this._mainFrame
: this._frames.get(framePayload.id);
? this.#mainFrame
: this.#frames.get(framePayload.id);
assert(
isMainFrame || frame,
'We either navigate top level or have old version of the navigated frame'
@ -378,21 +388,21 @@ export class FrameManager extends EventEmitter {
// Detach all child frames first.
if (frame) {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
this.#removeFramesRecursively(child);
}
// Update or create main frame.
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this._frames.delete(frame._id);
this.#frames.delete(frame._id);
frame._id = framePayload.id;
} else {
// Initial main frame navigation.
frame = new Frame(this, null, framePayload.id, this._client);
frame = new Frame(this, null, framePayload.id, this.#client);
}
this._frames.set(framePayload.id, frame);
this._mainFrame = frame;
this.#frames.set(framePayload.id, frame);
this.#mainFrame = frame;
}
// Update frame payload.
@ -404,8 +414,8 @@ export class FrameManager extends EventEmitter {
async _ensureIsolatedWorld(session: CDPSession, name: string): Promise<void> {
const key = `${session.id()}:${name}`;
if (this._isolatedWorlds.has(key)) return;
this._isolatedWorlds.add(key);
if (this.#isolatedWorlds.has(key)) return;
this.#isolatedWorlds.add(key);
await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
@ -414,7 +424,7 @@ export class FrameManager extends EventEmitter {
// Frames might be removed before we send this.
await Promise.all(
this.frames()
.filter((frame) => frame._client === session)
.filter((frame) => frame._client() === session)
.map((frame) =>
session
.send('Page.createIsolatedWorld', {
@ -427,41 +437,41 @@ export class FrameManager extends EventEmitter {
);
}
_onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this._frames.get(frameId);
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this.#frames.get(frameId);
if (!frame) return;
frame._navigatedWithinDocument(url);
this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
}
_onFrameDetached(
#onFrameDetached(
frameId: string,
reason: Protocol.Page.FrameDetachedEventReason
): void {
const frame = this._frames.get(frameId);
const frame = this.#frames.get(frameId);
if (reason === 'remove') {
// Only remove the frame if the reason for the detached event is
// an actual removement of the frame.
// For frames that become OOP iframes, the reason would be 'swap'.
if (frame) this._removeFramesRecursively(frame);
if (frame) this.#removeFramesRecursively(frame);
} else if (reason === 'swap') {
this.emit(FrameManagerEmittedEvents.FrameSwapped, frame);
}
}
_onExecutionContextCreated(
#onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription,
session: CDPSession
): void {
const auxData = contextPayload.auxData as { frameId?: string } | undefined;
const frameId = auxData && auxData.frameId;
const frame =
typeof frameId === 'string' ? this._frames.get(frameId) : undefined;
typeof frameId === 'string' ? this.#frames.get(frameId) : undefined;
let world: DOMWorld | undefined;
if (frame) {
// Only care about execution contexts created for the current session.
if (frame._client !== session) return;
if (frame._client() !== session) return;
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld;
@ -476,51 +486,51 @@ export class FrameManager extends EventEmitter {
}
}
const context = new ExecutionContext(
frame?._client || this._client,
frame?._client() || this.#client,
contextPayload,
world
);
if (world) world._setContext(context);
const key = `${session.id()}:${contextPayload.id}`;
this._contextIdToContext.set(key, context);
this.#contextIdToContext.set(key, context);
}
private _onExecutionContextDestroyed(
#onExecutionContextDestroyed(
executionContextId: number,
session: CDPSession
): void {
const key = `${session.id()}:${executionContextId}`;
const context = this._contextIdToContext.get(key);
const context = this.#contextIdToContext.get(key);
if (!context) return;
this._contextIdToContext.delete(key);
this.#contextIdToContext.delete(key);
if (context._world) context._world._setContext(null);
}
private _onExecutionContextsCleared(session: CDPSession): void {
for (const [key, context] of this._contextIdToContext.entries()) {
#onExecutionContextsCleared(session: CDPSession): void {
for (const [key, context] of this.#contextIdToContext.entries()) {
// Make sure to only clear execution contexts that belong
// to the current session.
if (context._client !== session) continue;
if (context._world) context._world._setContext(null);
this._contextIdToContext.delete(key);
this.#contextIdToContext.delete(key);
}
}
executionContextById(
contextId: number,
session: CDPSession = this._client
session: CDPSession = this.#client
): ExecutionContext {
const key = `${session.id()}:${contextId}`;
const context = this._contextIdToContext.get(key);
const context = this.#contextIdToContext.get(key);
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
return context;
}
private _removeFramesRecursively(frame: Frame): void {
#removeFramesRecursively(frame: Frame): void {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
this.#removeFramesRecursively(child);
frame._detach();
this._frames.delete(frame._id);
this.#frames.delete(frame._id);
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
}
}
@ -646,18 +656,19 @@ export interface FrameAddStyleTagOptions {
* @public
*/
export class Frame {
#parentFrame: Frame | null;
#url = '';
#detached = false;
#client!: CDPSession;
/**
* @internal
*/
_frameManager: FrameManager;
private _parentFrame: Frame | null;
/**
* @internal
*/
_id: string;
private _url = '';
private _detached = false;
/**
* @internal
*/
@ -670,7 +681,6 @@ export class Frame {
* @internal
*/
_hasStartedLoading = false;
/**
* @internal
*/
@ -687,10 +697,6 @@ export class Frame {
* @internal
*/
_childFrames: Set<Frame>;
/**
* @internal
*/
_client!: CDPSession;
/**
* @internal
@ -702,15 +708,15 @@ export class Frame {
client: CDPSession
) {
this._frameManager = frameManager;
this._parentFrame = parentFrame ?? null;
this._url = '';
this.#parentFrame = parentFrame ?? null;
this.#url = '';
this._id = frameId;
this._detached = false;
this.#detached = false;
this._loaderId = '';
this._childFrames = new Set();
if (this._parentFrame) this._parentFrame._childFrames.add(this);
if (this.#parentFrame) this.#parentFrame._childFrames.add(this);
this._updateClient(client);
}
@ -719,15 +725,15 @@ export class Frame {
* @internal
*/
_updateClient(client: CDPSession): void {
this._client = client;
this.#client = client;
this._mainWorld = new DOMWorld(
this._client,
this.#client,
this._frameManager,
this,
this._frameManager._timeoutSettings
);
this._secondaryWorld = new DOMWorld(
this._client,
this.#client,
this._frameManager,
this,
this._frameManager._timeoutSettings
@ -740,7 +746,7 @@ export class Frame {
* @returns `true` if the frame is an OOP frame, or `false` otherwise.
*/
isOOPFrame(): boolean {
return this._client !== this._frameManager._client;
return this.#client !== this._frameManager._client;
}
/**
@ -825,8 +831,8 @@ export class Frame {
/**
* @internal
*/
client(): CDPSession {
return this._client;
_client(): CDPSession {
return this.#client;
}
/**
@ -1008,14 +1014,14 @@ export class Frame {
* @returns the frame's URL.
*/
url(): string {
return this._url;
return this.#url;
}
/**
* @returns the parent `Frame`, if any. Detached and main frames return `null`.
*/
parentFrame(): Frame | null {
return this._parentFrame;
return this.#parentFrame;
}
/**
@ -1029,7 +1035,7 @@ export class Frame {
* @returns `true` if the frame has been detached, or `false` otherwise.
*/
isDetached(): boolean {
return this._detached;
return this.#detached;
}
/**
@ -1407,14 +1413,14 @@ export class Frame {
*/
_navigated(framePayload: Protocol.Page.Frame): void {
this._name = framePayload.name;
this._url = `${framePayload.url}${framePayload.urlFragment || ''}`;
this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
}
/**
* @internal
*/
_navigatedWithinDocument(url: string): void {
this._url = url;
this.#url = url;
}
/**
@ -1447,11 +1453,11 @@ export class Frame {
* @internal
*/
_detach(): void {
this._detached = true;
this.#detached = true;
this._mainWorld._detach();
this._secondaryWorld._detach();
if (this._parentFrame) this._parentFrame._childFrames.delete(this);
this._parentFrame = null;
if (this.#parentFrame) this.#parentFrame._childFrames.delete(this);
this.#parentFrame = null;
}
}

View File

@ -138,25 +138,25 @@ export class HTTPRequest {
*/
_redirectChain: HTTPRequest[];
private _client: CDPSession;
private _isNavigationRequest: boolean;
private _allowInterception: boolean;
private _interceptionHandled = false;
private _url: string;
private _resourceType: ResourceType;
#client: CDPSession;
#isNavigationRequest: boolean;
#allowInterception: boolean;
#interceptionHandled = false;
#url: string;
#resourceType: ResourceType;
private _method: string;
private _postData?: string;
private _headers: Record<string, string> = {};
private _frame: Frame | null;
private _continueRequestOverrides: ContinueRequestOverrides;
private _responseForRequest: Partial<ResponseForRequest> | null = null;
private _abortErrorReason: Protocol.Network.ErrorReason | null = null;
private _interceptResolutionState: InterceptResolutionState = {
#method: string;
#postData?: string;
#headers: Record<string, string> = {};
#frame: Frame | null;
#continueRequestOverrides: ContinueRequestOverrides;
#responseForRequest: Partial<ResponseForRequest> | null = null;
#abortErrorReason: Protocol.Network.ErrorReason | null = null;
#interceptResolutionState: InterceptResolutionState = {
action: InterceptResolutionAction.None,
};
private _interceptHandlers: Array<() => void | PromiseLike<any>>;
private _initiator: Protocol.Network.Initiator;
#interceptHandlers: Array<() => void | PromiseLike<any>>;
#initiator: Protocol.Network.Initiator;
/**
* @internal
@ -169,31 +169,31 @@ export class HTTPRequest {
event: Protocol.Network.RequestWillBeSentEvent,
redirectChain: HTTPRequest[]
) {
this._client = client;
this.#client = client;
this._requestId = event.requestId;
this._isNavigationRequest =
this.#isNavigationRequest =
event.requestId === event.loaderId && event.type === 'Document';
this._interceptionId = interceptionId;
this._allowInterception = allowInterception;
this._url = event.request.url;
this._resourceType = (event.type || 'other').toLowerCase() as ResourceType;
this._method = event.request.method;
this._postData = event.request.postData;
this._frame = frame;
this.#allowInterception = allowInterception;
this.#url = event.request.url;
this.#resourceType = (event.type || 'other').toLowerCase() as ResourceType;
this.#method = event.request.method;
this.#postData = event.request.postData;
this.#frame = frame;
this._redirectChain = redirectChain;
this._continueRequestOverrides = {};
this._interceptHandlers = [];
this._initiator = event.initiator;
this.#continueRequestOverrides = {};
this.#interceptHandlers = [];
this.#initiator = event.initiator;
for (const [key, value] of Object.entries(event.request.headers))
this._headers[key.toLowerCase()] = value;
this.#headers[key.toLowerCase()] = value;
}
/**
* @returns the URL of the request
*/
url(): string {
return this._url;
return this.#url;
}
/**
@ -202,8 +202,8 @@ export class HTTPRequest {
* `respond()` aren't called).
*/
continueRequestOverrides(): ContinueRequestOverrides {
assert(this._allowInterception, 'Request Interception is not enabled!');
return this._continueRequestOverrides;
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#continueRequestOverrides;
}
/**
@ -211,16 +211,16 @@ export class HTTPRequest {
* interception is allowed to respond (ie, `abort()` is not called).
*/
responseForRequest(): Partial<ResponseForRequest> | null {
assert(this._allowInterception, 'Request Interception is not enabled!');
return this._responseForRequest;
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#responseForRequest;
}
/**
* @returns the most recent reason for aborting the request
*/
abortErrorReason(): Protocol.Network.ErrorReason | null {
assert(this._allowInterception, 'Request Interception is not enabled!');
return this._abortErrorReason;
assert(this.#allowInterception, 'Request Interception is not enabled!');
return this.#abortErrorReason;
}
/**
@ -235,11 +235,11 @@ export class HTTPRequest {
* `disabled`, `none`, or `already-handled`.
*/
interceptResolutionState(): InterceptResolutionState {
if (!this._allowInterception)
if (!this.#allowInterception)
return { action: InterceptResolutionAction.Disabled };
if (this._interceptionHandled)
if (this.#interceptionHandled)
return { action: InterceptResolutionAction.AlreadyHandled };
return { ...this._interceptResolutionState };
return { ...this.#interceptResolutionState };
}
/**
@ -247,7 +247,7 @@ export class HTTPRequest {
* `false` otherwise.
*/
isInterceptResolutionHandled(): boolean {
return this._interceptionHandled;
return this.#interceptionHandled;
}
/**
@ -259,7 +259,7 @@ export class HTTPRequest {
enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>
): void {
this._interceptHandlers.push(pendingHandler);
this.#interceptHandlers.push(pendingHandler);
}
/**
@ -267,21 +267,21 @@ export class HTTPRequest {
* the request interception.
*/
async finalizeInterceptions(): Promise<void> {
await this._interceptHandlers.reduce(
await this.#interceptHandlers.reduce(
(promiseChain, interceptAction) => promiseChain.then(interceptAction),
Promise.resolve()
);
const { action } = this.interceptResolutionState();
switch (action) {
case 'abort':
return this._abort(this._abortErrorReason);
return this.#abort(this.#abortErrorReason);
case 'respond':
if (this._responseForRequest === null) {
if (this.#responseForRequest === null) {
throw new Error('Response is missing for the interception');
}
return this._respond(this._responseForRequest);
return this.#respond(this.#responseForRequest);
case 'continue':
return this._continue(this._continueRequestOverrides);
return this.#continue(this.#continueRequestOverrides);
}
}
@ -290,21 +290,21 @@ export class HTTPRequest {
* engine.
*/
resourceType(): ResourceType {
return this._resourceType;
return this.#resourceType;
}
/**
* @returns the method used (`GET`, `POST`, etc.)
*/
method(): string {
return this._method;
return this.#method;
}
/**
* @returns the request's post body, if any.
*/
postData(): string | undefined {
return this._postData;
return this.#postData;
}
/**
@ -312,7 +312,7 @@ export class HTTPRequest {
* header names are lower-case.
*/
headers(): Record<string, string> {
return this._headers;
return this.#headers;
}
/**
@ -328,21 +328,21 @@ export class HTTPRequest {
* error pages.
*/
frame(): Frame | null {
return this._frame;
return this.#frame;
}
/**
* @returns true if the request is the driver of the current frame's navigation.
*/
isNavigationRequest(): boolean {
return this._isNavigationRequest;
return this.#isNavigationRequest;
}
/**
* @returns the initiator of the request.
*/
initiator(): Protocol.Network.Initiator {
return this._initiator;
return this.#initiator;
}
/**
@ -436,41 +436,39 @@ export class HTTPRequest {
priority?: number
): Promise<void> {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:')) return;
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
if (this.#url.startsWith('data:')) return;
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return this._continue(overrides);
return this.#continue(overrides);
}
this._continueRequestOverrides = overrides;
this.#continueRequestOverrides = overrides;
if (
this._interceptResolutionState.priority === undefined ||
priority > this._interceptResolutionState.priority
this.#interceptResolutionState.priority === undefined ||
priority > this.#interceptResolutionState.priority
) {
this._interceptResolutionState = {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Continue,
priority,
};
return;
}
if (priority === this._interceptResolutionState.priority) {
if (priority === this.#interceptResolutionState.priority) {
if (
this._interceptResolutionState.action === 'abort' ||
this._interceptResolutionState.action === 'respond'
this.#interceptResolutionState.action === 'abort' ||
this.#interceptResolutionState.action === 'respond'
) {
return;
}
this._interceptResolutionState.action =
this.#interceptResolutionState.action =
InterceptResolutionAction.Continue;
}
return;
}
private async _continue(
overrides: ContinueRequestOverrides = {}
): Promise<void> {
async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const { url, method, postData, headers } = overrides;
this._interceptionHandled = true;
this.#interceptionHandled = true;
const postDataBinaryBase64 = postData
? Buffer.from(postData).toString('base64')
@ -480,7 +478,7 @@ export class HTTPRequest {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest'
);
await this._client
await this.#client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
@ -489,7 +487,7 @@ export class HTTPRequest {
headers: headers ? headersArray(headers) : undefined,
})
.catch((error) => {
this._interceptionHandled = false;
this.#interceptionHandled = false;
return handleError(error);
});
}
@ -530,33 +528,33 @@ export class HTTPRequest {
priority?: number
): Promise<void> {
// Mocking responses for dataURL requests is not currently supported.
if (this._url.startsWith('data:')) return;
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
if (this.#url.startsWith('data:')) return;
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return this._respond(response);
return this.#respond(response);
}
this._responseForRequest = response;
this.#responseForRequest = response;
if (
this._interceptResolutionState.priority === undefined ||
priority > this._interceptResolutionState.priority
this.#interceptResolutionState.priority === undefined ||
priority > this.#interceptResolutionState.priority
) {
this._interceptResolutionState = {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Respond,
priority,
};
return;
}
if (priority === this._interceptResolutionState.priority) {
if (this._interceptResolutionState.action === 'abort') {
if (priority === this.#interceptResolutionState.priority) {
if (this.#interceptResolutionState.action === 'abort') {
return;
}
this._interceptResolutionState.action = InterceptResolutionAction.Respond;
this.#interceptResolutionState.action = InterceptResolutionAction.Respond;
}
}
private async _respond(response: Partial<ResponseForRequest>): Promise<void> {
this._interceptionHandled = true;
async #respond(response: Partial<ResponseForRequest>): Promise<void> {
this.#interceptionHandled = true;
const responseBody: Buffer | null =
response.body && helper.isString(response.body)
@ -585,7 +583,7 @@ export class HTTPRequest {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest'
);
await this._client
await this.#client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: status,
@ -594,7 +592,7 @@ export class HTTPRequest {
body: responseBody ? responseBody.toString('base64') : undefined,
})
.catch((error) => {
this._interceptionHandled = false;
this.#interceptionHandled = false;
return handleError(error);
});
}
@ -617,20 +615,20 @@ export class HTTPRequest {
priority?: number
): Promise<void> {
// Request interception is not supported for data: urls.
if (this._url.startsWith('data:')) return;
if (this.#url.startsWith('data:')) return;
const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode);
assert(this._allowInterception, 'Request Interception is not enabled!');
assert(!this._interceptionHandled, 'Request is already handled!');
assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) {
return this._abort(errorReason);
return this.#abort(errorReason);
}
this._abortErrorReason = errorReason;
this.#abortErrorReason = errorReason;
if (
this._interceptResolutionState.priority === undefined ||
priority >= this._interceptResolutionState.priority
this.#interceptResolutionState.priority === undefined ||
priority >= this.#interceptResolutionState.priority
) {
this._interceptResolutionState = {
this.#interceptResolutionState = {
action: InterceptResolutionAction.Abort,
priority,
};
@ -638,15 +636,15 @@ export class HTTPRequest {
}
}
private async _abort(
async #abort(
errorReason: Protocol.Network.ErrorReason | null
): Promise<void> {
this._interceptionHandled = true;
this.#interceptionHandled = true;
if (this._interceptionId === undefined)
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
);
await this._client
await this.#client
.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason: errorReason || 'Failed',

View File

@ -44,20 +44,20 @@ interface CDPSession extends EventEmitter {
* @public
*/
export class HTTPResponse {
private _client: CDPSession;
private _request: HTTPRequest;
private _contentPromise: Promise<Buffer> | null = null;
private _bodyLoadedPromise: Promise<Error | void>;
private _bodyLoadedPromiseFulfill: (err: Error | void) => void = () => {};
private _remoteAddress: RemoteAddress;
private _status: number;
private _statusText: string;
private _url: string;
private _fromDiskCache: boolean;
private _fromServiceWorker: boolean;
private _headers: Record<string, string> = {};
private _securityDetails: SecurityDetails | null;
private _timing: Protocol.Network.ResourceTiming | null;
#client: CDPSession;
#request: HTTPRequest;
#contentPromise: Promise<Buffer> | null = null;
#bodyLoadedPromise: Promise<Error | void>;
#bodyLoadedPromiseFulfill: (err: Error | void) => void = () => {};
#remoteAddress: RemoteAddress;
#status: number;
#statusText: string;
#url: string;
#fromDiskCache: boolean;
#fromServiceWorker: boolean;
#headers: Record<string, string> = {};
#securityDetails: SecurityDetails | null;
#timing: Protocol.Network.ResourceTiming | null;
/**
* @internal
@ -68,40 +68,37 @@ export class HTTPResponse {
responsePayload: Protocol.Network.Response,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
) {
this._client = client;
this._request = request;
this.#client = client;
this.#request = request;
this._bodyLoadedPromise = new Promise((fulfill) => {
this._bodyLoadedPromiseFulfill = fulfill;
this.#bodyLoadedPromise = new Promise((fulfill) => {
this.#bodyLoadedPromiseFulfill = fulfill;
});
this._remoteAddress = {
this.#remoteAddress = {
ip: responsePayload.remoteIPAddress,
port: responsePayload.remotePort,
};
this._statusText =
this._parseStatusTextFromExtrInfo(extraInfo) ||
this.#statusText =
this.#parseStatusTextFromExtrInfo(extraInfo) ||
responsePayload.statusText;
this._url = request.url();
this._fromDiskCache = !!responsePayload.fromDiskCache;
this._fromServiceWorker = !!responsePayload.fromServiceWorker;
this.#url = request.url();
this.#fromDiskCache = !!responsePayload.fromDiskCache;
this.#fromServiceWorker = !!responsePayload.fromServiceWorker;
this._status = extraInfo ? extraInfo.statusCode : responsePayload.status;
this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status;
const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
for (const [key, value] of Object.entries(headers)) {
this._headers[key.toLowerCase()] = value;
this.#headers[key.toLowerCase()] = value;
}
this._securityDetails = responsePayload.securityDetails
this.#securityDetails = responsePayload.securityDetails
? new SecurityDetails(responsePayload.securityDetails)
: null;
this._timing = responsePayload.timing || null;
this.#timing = responsePayload.timing || null;
}
/**
* @internal
*/
_parseStatusTextFromExtrInfo(
#parseStatusTextFromExtrInfo(
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): string | undefined {
if (!extraInfo || !extraInfo.headersText) return;
@ -119,9 +116,9 @@ export class HTTPResponse {
*/
_resolveBody(err: Error | null): void {
if (err) {
return this._bodyLoadedPromiseFulfill(err);
return this.#bodyLoadedPromiseFulfill(err);
}
return this._bodyLoadedPromiseFulfill();
return this.#bodyLoadedPromiseFulfill();
}
/**
@ -129,14 +126,14 @@ export class HTTPResponse {
* server.
*/
remoteAddress(): RemoteAddress {
return this._remoteAddress;
return this.#remoteAddress;
}
/**
* @returns The URL of the response.
*/
url(): string {
return this._url;
return this.#url;
}
/**
@ -144,14 +141,14 @@ export class HTTPResponse {
*/
ok(): boolean {
// TODO: document === 0 case?
return this._status === 0 || (this._status >= 200 && this._status <= 299);
return this.#status === 0 || (this.#status >= 200 && this.#status <= 299);
}
/**
* @returns The status code of the response (e.g., 200 for a success).
*/
status(): number {
return this._status;
return this.#status;
}
/**
@ -159,7 +156,7 @@ export class HTTPResponse {
* success).
*/
statusText(): string {
return this._statusText;
return this.#statusText;
}
/**
@ -167,7 +164,7 @@ export class HTTPResponse {
* header names are lower-case.
*/
headers(): Record<string, string> {
return this._headers;
return this.#headers;
}
/**
@ -175,26 +172,26 @@ export class HTTPResponse {
* secure connection, or `null` otherwise.
*/
securityDetails(): SecurityDetails | null {
return this._securityDetails;
return this.#securityDetails;
}
/**
* @returns Timing information related to the response.
*/
timing(): Protocol.Network.ResourceTiming | null {
return this._timing;
return this.#timing;
}
/**
* @returns Promise which resolves to a buffer with response body.
*/
buffer(): Promise<Buffer> {
if (!this._contentPromise) {
this._contentPromise = this._bodyLoadedPromise.then(async (error) => {
if (!this.#contentPromise) {
this.#contentPromise = this.#bodyLoadedPromise.then(async (error) => {
if (error) throw error;
try {
const response = await this._client.send('Network.getResponseBody', {
requestId: this._request._requestId,
const response = await this.#client.send('Network.getResponseBody', {
requestId: this.#request._requestId,
});
return Buffer.from(
response.body,
@ -214,7 +211,7 @@ export class HTTPResponse {
}
});
}
return this._contentPromise;
return this.#contentPromise;
}
/**
@ -243,7 +240,7 @@ export class HTTPResponse {
* @returns A matching {@link HTTPRequest} object.
*/
request(): HTTPRequest {
return this._request;
return this.#request;
}
/**
@ -251,14 +248,14 @@ export class HTTPResponse {
* cache or memory cache.
*/
fromCache(): boolean {
return this._fromDiskCache || this._request._fromMemoryCache;
return this.#fromDiskCache || this.#request._fromMemoryCache;
}
/**
* @returns True if the response was served by a service worker.
*/
fromServiceWorker(): boolean {
return this._fromServiceWorker;
return this.#fromServiceWorker;
}
/**
@ -266,6 +263,6 @@ export class HTTPResponse {
* navigating to error pages.
*/
frame(): Frame | null {
return this._request.frame();
return this.#request.frame();
}
}

View File

@ -16,7 +16,11 @@
import { assert } from './assert.js';
import { CDPSession } from './Connection.js';
import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout.js';
import {
_keyDefinitions,
KeyDefinition,
KeyInput,
} from './USKeyboardLayout.js';
import { Protocol } from 'devtools-protocol';
import { Point } from './JSHandle.js';
@ -64,14 +68,19 @@ type KeyDescription = Required<
* @public
*/
export class Keyboard {
private _client: CDPSession;
/** @internal */
_modifiers = 0;
private _pressedKeys = new Set<string>();
#client: CDPSession;
#pressedKeys = new Set<string>();
/** @internal */
/**
* @internal
*/
_modifiers = 0;
/**
* @internal
*/
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
/**
@ -103,14 +112,14 @@ export class Keyboard {
key: KeyInput,
options: { text?: string } = { text: undefined }
): Promise<void> {
const description = this._keyDescriptionForString(key);
const description = this.#keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key);
const autoRepeat = this.#pressedKeys.has(description.code);
this.#pressedKeys.add(description.code);
this._modifiers |= this.#modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text;
await this._client.send('Input.dispatchKeyEvent', {
await this.#client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode,
@ -124,7 +133,7 @@ export class Keyboard {
});
}
private _modifierBit(key: string): number {
#modifierBit(key: string): number {
if (key === 'Alt') return 1;
if (key === 'Control') return 2;
if (key === 'Meta') return 4;
@ -132,7 +141,7 @@ export class Keyboard {
return 0;
}
private _keyDescriptionForString(keyString: KeyInput): KeyDescription {
#keyDescriptionForString(keyString: KeyInput): KeyDescription {
const shift = this._modifiers & 8;
const description = {
key: '',
@ -142,7 +151,7 @@ export class Keyboard {
location: 0,
};
const definition = keyDefinitions[keyString];
const definition = _keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
if (definition.key) description.key = definition.key;
@ -175,11 +184,11 @@ export class Keyboard {
* for a list of all key names.
*/
async up(key: KeyInput): Promise<void> {
const description = this._keyDescriptionForString(key);
const description = this.#keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
this._pressedKeys.delete(description.code);
await this._client.send('Input.dispatchKeyEvent', {
this._modifiers &= ~this.#modifierBit(description.key);
this.#pressedKeys.delete(description.code);
await this.#client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this._modifiers,
key: description.key,
@ -205,11 +214,11 @@ export class Keyboard {
* @param char - Character to send into the page.
*/
async sendCharacter(char: string): Promise<void> {
await this._client.send('Input.insertText', { text: char });
await this.#client.send('Input.insertText', { text: char });
}
private charIsKey(char: string): char is KeyInput {
return !!keyDefinitions[char as KeyInput];
return !!_keyDefinitions[char as KeyInput];
}
/**
@ -356,18 +365,18 @@ export interface MouseWheelOptions {
* @public
*/
export class Mouse {
private _client: CDPSession;
private _keyboard: Keyboard;
private _x = 0;
private _y = 0;
private _button: MouseButton | 'none' = 'none';
#client: CDPSession;
#keyboard: Keyboard;
#x = 0;
#y = 0;
#button: MouseButton | 'none' = 'none';
/**
* @internal
*/
constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client;
this._keyboard = keyboard;
this.#client = client;
this.#keyboard = keyboard;
}
/**
@ -383,17 +392,17 @@ export class Mouse {
options: { steps?: number } = {}
): Promise<void> {
const { steps = 1 } = options;
const fromX = this._x,
fromY = this._y;
this._x = x;
this._y = y;
const fromX = this.#x,
fromY = this.#y;
this.#x = x;
this.#y = y;
for (let i = 1; i <= steps; i++) {
await this._client.send('Input.dispatchMouseEvent', {
await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers,
button: this.#button,
x: fromX + (this.#x - fromX) * (i / steps),
y: fromY + (this.#y - fromY) * (i / steps),
modifiers: this.#keyboard._modifiers,
});
}
}
@ -428,13 +437,13 @@ export class Mouse {
*/
async down(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options;
this._button = button;
await this._client.send('Input.dispatchMouseEvent', {
this.#button = button;
await this.#client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
button,
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
x: this.#x,
y: this.#y,
modifiers: this.#keyboard._modifiers,
clickCount,
});
}
@ -445,13 +454,13 @@ export class Mouse {
*/
async up(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options;
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
this.#button = 'none';
await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button,
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
x: this.#x,
y: this.#y,
modifiers: this.#keyboard._modifiers,
clickCount,
});
}
@ -477,13 +486,13 @@ export class Mouse {
*/
async wheel(options: MouseWheelOptions = {}): Promise<void> {
const { deltaX = 0, deltaY = 0 } = options;
await this._client.send('Input.dispatchMouseEvent', {
await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x: this._x,
y: this._y,
x: this.#x,
y: this.#y,
deltaX,
deltaY,
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
pointerType: 'mouse',
});
}
@ -495,7 +504,7 @@ export class Mouse {
*/
async drag(start: Point, target: Point): Promise<Protocol.Input.DragData> {
const promise = new Promise<Protocol.Input.DragData>((resolve) => {
this._client.once('Input.dragIntercepted', (event) =>
this.#client.once('Input.dragIntercepted', (event) =>
resolve(event.data)
);
});
@ -511,11 +520,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask
*/
async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', {
await this.#client.send('Input.dispatchDragEvent', {
type: 'dragEnter',
x: target.x,
y: target.y,
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
data,
});
}
@ -526,11 +535,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask
*/
async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', {
await this.#client.send('Input.dispatchDragEvent', {
type: 'dragOver',
x: target.x,
y: target.y,
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
data,
});
}
@ -541,11 +550,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask
*/
async drop(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', {
await this.#client.send('Input.dispatchDragEvent', {
type: 'drop',
x: target.x,
y: target.y,
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
data,
});
}
@ -580,15 +589,15 @@ export class Mouse {
* @public
*/
export class Touchscreen {
private _client: CDPSession;
private _keyboard: Keyboard;
#client: CDPSession;
#keyboard: Keyboard;
/**
* @internal
*/
constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client;
this._keyboard = keyboard;
this.#client = client;
this.#keyboard = keyboard;
}
/**
@ -598,15 +607,15 @@ export class Touchscreen {
*/
async tap(x: number, y: number): Promise<void> {
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this._client.send('Input.dispatchTouchEvent', {
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints,
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
});
await this._client.send('Input.dispatchTouchEvent', {
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
modifiers: this._keyboard._modifiers,
modifiers: this.#keyboard._modifiers,
});
}
}

View File

@ -30,7 +30,7 @@ import { Frame, FrameManager } from './FrameManager.js';
import { debugError, helper } from './helper.js';
import { MouseButton } from './Input.js';
import { Page, ScreenshotOptions } from './Page.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { _getQueryHandlerAndSelector } from './QueryHandler.js';
import { KeyInput } from './USKeyboardLayout.js';
/**
@ -62,7 +62,7 @@ export interface BoundingBox extends Point {
/**
* @internal
*/
export function createJSHandle(
export function _createJSHandle(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle {
@ -103,22 +103,38 @@ const applyOffsetsToQuad = (quad: Point[], offsetX: number, offsetY: number) =>
* @public
*/
export class JSHandle<HandleObjectType = unknown> {
#client: CDPSession;
#disposed = false;
#context: ExecutionContext;
#remoteObject: Protocol.Runtime.RemoteObject;
/**
* @internal
*/
_context: ExecutionContext;
get _client(): CDPSession {
return this.#client;
}
/**
* @internal
*/
_client: CDPSession;
get _disposed(): boolean {
return this.#disposed;
}
/**
* @internal
*/
_remoteObject: Protocol.Runtime.RemoteObject;
get _remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
/**
* @internal
*/
_disposed = false;
get _context(): ExecutionContext {
return this.#context;
}
/**
* @internal
@ -128,15 +144,15 @@ export class JSHandle<HandleObjectType = unknown> {
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject
) {
this._context = context;
this._client = client;
this._remoteObject = remoteObject;
this.#context = context;
this.#client = client;
this.#remoteObject = remoteObject;
}
/** Returns the execution context the handle belongs to.
*/
executionContext(): ExecutionContext {
return this._context;
return this.#context;
}
/**
@ -222,15 +238,15 @@ export class JSHandle<HandleObjectType = unknown> {
* ```
*/
async getProperties(): Promise<Map<string, JSHandle>> {
assert(this._remoteObject.objectId);
const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId,
assert(this.#remoteObject.objectId);
const response = await this.#client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId,
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable || !property.value) continue;
result.set(property.name, createJSHandle(this._context, property.value));
result.set(property.name, _createJSHandle(this.#context, property.value));
}
return result;
}
@ -245,16 +261,16 @@ export class JSHandle<HandleObjectType = unknown> {
* **NOTE** The method throws if the referenced object is not stringifiable.
*/
async jsonValue<T = unknown>(): Promise<T> {
if (this._remoteObject.objectId) {
const response = await this._client.send('Runtime.callFunctionOn', {
if (this.#remoteObject.objectId) {
const response = await this.#client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }',
objectId: this._remoteObject.objectId,
objectId: this.#remoteObject.objectId,
returnByValue: true,
awaitPromise: true,
});
return helper.valueFromRemoteObject(response.result) as T;
}
return helper.valueFromRemoteObject(this._remoteObject) as T;
return helper.valueFromRemoteObject(this.#remoteObject) as T;
}
/**
@ -273,9 +289,9 @@ export class JSHandle<HandleObjectType = unknown> {
* successfully disposed of.
*/
async dispose(): Promise<void> {
if (this._disposed) return;
this._disposed = true;
await helper.releaseObject(this._client, this._remoteObject);
if (this.#disposed) return;
this.#disposed = true;
await helper.releaseObject(this.#client, this.#remoteObject);
}
/**
@ -284,11 +300,11 @@ export class JSHandle<HandleObjectType = unknown> {
* @remarks Useful during debugging.
*/
toString(): string {
if (this._remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type;
if (this.#remoteObject.objectId) {
const type = this.#remoteObject.subtype || this.#remoteObject.type;
return 'JSHandle@' + type;
}
return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
return 'JSHandle:' + helper.valueFromRemoteObject(this.#remoteObject);
}
}
@ -329,9 +345,9 @@ export class JSHandle<HandleObjectType = unknown> {
export class ElementHandle<
ElementType extends Element = Element
> extends JSHandle<ElementType> {
private _frame: Frame;
private _page: Page;
private _frameManager: FrameManager;
#frame: Frame;
#page: Page;
#frameManager: FrameManager;
/**
* @internal
@ -345,11 +361,9 @@ export class ElementHandle<
frameManager: FrameManager
) {
super(context, client, remoteObject);
this._client = client;
this._remoteObject = remoteObject;
this._frame = frame;
this._page = page;
this._frameManager = frameManager;
this.#frame = frame;
this.#page = page;
this.#frameManager = frameManager;
}
/**
@ -498,10 +512,10 @@ export class ElementHandle<
objectId: this._remoteObject.objectId,
});
if (typeof nodeInfo.node.frameId !== 'string') return null;
return this._frameManager.frame(nodeInfo.node.frameId);
return this.#frameManager.frame(nodeInfo.node.frameId);
}
private async _scrollIntoViewIfNeeded(): Promise<void> {
async #scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate(
async (
element: Element,
@ -541,13 +555,13 @@ export class ElementHandle<
}
return false;
},
this._page.isJavaScriptEnabled()
this.#page.isJavaScriptEnabled()
);
if (error) throw new Error(error);
}
private async _getOOPIFOffsets(
async #getOOPIFOffsets(
frame: Frame
): Promise<{ offsetX: number; offsetY: number }> {
let offsetX = 0;
@ -559,17 +573,19 @@ export class ElementHandle<
currentFrame = parent;
continue;
}
const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', {
frameId: currentFrame._id,
});
const result = await parent._client.send('DOM.getBoxModel', {
const { backendNodeId } = await parent
._client()
.send('DOM.getFrameOwner', {
frameId: currentFrame._id,
});
const result = await parent._client().send('DOM.getBoxModel', {
backendNodeId: backendNodeId,
});
if (!result) {
break;
}
const contentBoxQuad = result.model.content;
const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0];
const topLeftCorner = this.#fromProtocolQuad(contentBoxQuad)[0];
offsetX += topLeftCorner!.x;
offsetY += topLeftCorner!.y;
currentFrame = parent;
@ -587,7 +603,7 @@ export class ElementHandle<
objectId: this._remoteObject.objectId,
})
.catch(debugError),
this._page.client().send('Page.getLayoutMetrics'),
this.#page._client().send('Page.getLayoutMetrics'),
]);
if (!result || !result.quads.length)
throw new Error('Node is either not clickable or not an HTMLElement');
@ -595,12 +611,12 @@ export class ElementHandle<
// Fallback to `layoutViewport` in case of using Firefox.
const { clientWidth, clientHeight } =
layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
const quads = result.quads
.map((quad) => this._fromProtocolQuad(quad))
.map((quad) => this.#fromProtocolQuad(quad))
.map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY))
.map((quad) =>
this._intersectQuadWithViewport(quad, clientWidth, clientHeight)
this.#intersectQuadWithViewport(quad, clientWidth, clientHeight)
)
.filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length)
@ -641,7 +657,7 @@ export class ElementHandle<
};
}
private _getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
#getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this._remoteObject.objectId,
};
@ -650,7 +666,7 @@ export class ElementHandle<
.catch((error) => debugError(error));
}
private _fromProtocolQuad(quad: number[]): Point[] {
#fromProtocolQuad(quad: number[]): Point[] {
return [
{ x: quad[0]!, y: quad[1]! },
{ x: quad[2]!, y: quad[3]! },
@ -659,7 +675,7 @@ export class ElementHandle<
];
}
private _intersectQuadWithViewport(
#intersectQuadWithViewport(
quad: Point[],
width: number,
height: number
@ -676,9 +692,9 @@ export class ElementHandle<
* If the element is detached from DOM, the method throws an error.
*/
async hover(): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint();
await this._page.mouse.move(x, y);
await this.#page.mouse.move(x, y);
}
/**
@ -687,9 +703,9 @@ export class ElementHandle<
* If the element is detached from DOM, the method throws an error.
*/
async click(options: ClickOptions = {}): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint(options.offset);
await this._page.mouse.click(x, y, options);
await this.#page.mouse.click(x, y, options);
}
/**
@ -697,12 +713,12 @@ export class ElementHandle<
*/
async drag(target: Point): Promise<Protocol.Input.DragData> {
assert(
this._page.isDragInterceptionEnabled(),
this.#page.isDragInterceptionEnabled(),
'Drag Interception is not enabled!'
);
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const start = await this.clickablePoint();
return await this._page.mouse.drag(start, target);
return await this.#page.mouse.drag(start, target);
}
/**
@ -711,9 +727,9 @@ export class ElementHandle<
async dragEnter(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await this._page.mouse.dragEnter(target, data);
await this.#page.mouse.dragEnter(target, data);
}
/**
@ -722,9 +738,9 @@ export class ElementHandle<
async dragOver(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await this._page.mouse.dragOver(target, data);
await this.#page.mouse.dragOver(target, data);
}
/**
@ -733,9 +749,9 @@ export class ElementHandle<
async drop(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const destination = await this.clickablePoint();
await this._page.mouse.drop(destination, data);
await this.#page.mouse.drop(destination, data);
}
/**
@ -745,10 +761,10 @@ export class ElementHandle<
target: ElementHandle,
options?: { delay: number }
): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const startPoint = await this.clickablePoint();
const targetPoint = await target.clickablePoint();
await this._page.mouse.dragAndDrop(startPoint, targetPoint, options);
await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
/**
@ -883,9 +899,9 @@ export class ElementHandle<
* If the element is detached from DOM, the method throws an error.
*/
async tap(): Promise<void> {
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint();
await this._page.touchscreen.tap(x, y);
await this.#page.touchscreen.tap(x, y);
}
/**
@ -921,7 +937,7 @@ export class ElementHandle<
*/
async type(text: string, options?: { delay: number }): Promise<void> {
await this.focus();
await this._page.keyboard.type(text, options);
await this.#page.keyboard.type(text, options);
}
/**
@ -940,7 +956,7 @@ export class ElementHandle<
*/
async press(key: KeyInput, options?: PressOptions): Promise<void> {
await this.focus();
await this._page.keyboard.press(key, options);
await this.#page.keyboard.press(key, options);
}
/**
@ -948,11 +964,11 @@ export class ElementHandle<
* or `null` if the element is not visible.
*/
async boundingBox(): Promise<BoundingBox | null> {
const result = await this._getBoxModel();
const result = await this.#getBoxModel();
if (!result) return null;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
const quad = result.model.border;
const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!);
const y = Math.min(quad[1]!, quad[3]!, quad[5]!, quad[7]!);
@ -971,31 +987,31 @@ export class ElementHandle<
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/
async boxModel(): Promise<BoxModel | null> {
const result = await this._getBoxModel();
const result = await this.#getBoxModel();
if (!result) return null;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
const { content, padding, border, margin, width, height } = result.model;
return {
content: applyOffsetsToQuad(
this._fromProtocolQuad(content),
this.#fromProtocolQuad(content),
offsetX,
offsetY
),
padding: applyOffsetsToQuad(
this._fromProtocolQuad(padding),
this.#fromProtocolQuad(padding),
offsetX,
offsetY
),
border: applyOffsetsToQuad(
this._fromProtocolQuad(border),
this.#fromProtocolQuad(border),
offsetX,
offsetY
),
margin: applyOffsetsToQuad(
this._fromProtocolQuad(margin),
this.#fromProtocolQuad(margin),
offsetX,
offsetY
),
@ -1015,7 +1031,7 @@ export class ElementHandle<
let boundingBox = await this.boundingBox();
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
const viewport = this._page.viewport();
const viewport = this.#page.viewport();
assert(viewport);
if (
@ -1026,12 +1042,12 @@ export class ElementHandle<
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
};
await this._page.setViewport(Object.assign({}, viewport, newViewport));
await this.#page.setViewport(Object.assign({}, viewport, newViewport));
needsViewportReset = true;
}
await this._scrollIntoViewIfNeeded();
await this.#scrollIntoViewIfNeeded();
boundingBox = await this.boundingBox();
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
@ -1047,7 +1063,7 @@ export class ElementHandle<
clip.x += pageX;
clip.y += pageY;
const imageData = await this._page.screenshot(
const imageData = await this.#page.screenshot(
Object.assign(
{},
{
@ -1057,7 +1073,7 @@ export class ElementHandle<
)
);
if (needsViewportReset) await this._page.setViewport(viewport);
if (needsViewportReset) await this.#page.setViewport(viewport);
return imageData;
}
@ -1073,7 +1089,7 @@ export class ElementHandle<
selector: string
): Promise<ElementHandle<T> | null> {
const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector);
_getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryOne,
'Cannot handle queries for a single element with the given selector'
@ -1096,7 +1112,7 @@ export class ElementHandle<
selector: string
): Promise<Array<ElementHandle<T>>> {
const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector);
_getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector'
@ -1184,7 +1200,7 @@ export class ElementHandle<
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector);
_getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray);
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>(

View File

@ -60,41 +60,41 @@ const noop = (): void => {};
* @internal
*/
export class LifecycleWatcher {
_expectedLifecycle: ProtocolLifeCycleEvent[];
_frameManager: FrameManager;
_frame: Frame;
_timeout: number;
_navigationRequest: HTTPRequest | null = null;
_eventListeners: PuppeteerEventListener[];
#expectedLifecycle: ProtocolLifeCycleEvent[];
#frameManager: FrameManager;
#frame: Frame;
#timeout: number;
#navigationRequest: HTTPRequest | null = null;
#eventListeners: PuppeteerEventListener[];
_sameDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
_sameDocumentNavigationPromise = new Promise<Error | undefined>((fulfill) => {
this._sameDocumentNavigationCompleteCallback = fulfill;
#sameDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
#sameDocumentNavigationPromise = new Promise<Error | undefined>((fulfill) => {
this.#sameDocumentNavigationCompleteCallback = fulfill;
});
_lifecycleCallback: () => void = noop;
_lifecyclePromise: Promise<void> = new Promise((fulfill) => {
this._lifecycleCallback = fulfill;
#lifecycleCallback: () => void = noop;
#lifecyclePromise: Promise<void> = new Promise((fulfill) => {
this.#lifecycleCallback = fulfill;
});
_newDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
_newDocumentNavigationPromise: Promise<Error | undefined> = new Promise(
#newDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
#newDocumentNavigationPromise: Promise<Error | undefined> = new Promise(
(fulfill) => {
this._newDocumentNavigationCompleteCallback = fulfill;
this.#newDocumentNavigationCompleteCallback = fulfill;
}
);
_terminationCallback: (x?: Error) => void = noop;
_terminationPromise: Promise<Error | undefined> = new Promise((fulfill) => {
this._terminationCallback = fulfill;
#terminationCallback: (x?: Error) => void = noop;
#terminationPromise: Promise<Error | undefined> = new Promise((fulfill) => {
this.#terminationCallback = fulfill;
});
_timeoutPromise: Promise<TimeoutError | undefined>;
#timeoutPromise: Promise<TimeoutError | undefined>;
_maximumTimer?: NodeJS.Timeout;
_hasSameDocumentNavigation?: boolean;
_newDocumentNavigation?: boolean;
_swapped?: boolean;
#maximumTimer?: NodeJS.Timeout;
#hasSameDocumentNavigation?: boolean;
#newDocumentNavigation?: boolean;
#swapped?: boolean;
constructor(
frameManager: FrameManager,
@ -104,137 +104,138 @@ export class LifecycleWatcher {
) {
if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice();
else if (typeof waitUntil === 'string') waitUntil = [waitUntil];
this._expectedLifecycle = waitUntil.map((value) => {
this.#expectedLifecycle = waitUntil.map((value) => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent as ProtocolLifeCycleEvent;
});
this._frameManager = frameManager;
this._frame = frame;
this._timeout = timeout;
this._eventListeners = [
this.#frameManager = frameManager;
this.#frame = frame;
this.#timeout = timeout;
this.#eventListeners = [
helper.addEventListener(
frameManager._client,
CDPSessionEmittedEvents.Disconnected,
this._terminate.bind(
this.#terminate.bind(
this,
new Error('Navigation failed because browser has disconnected!')
)
),
helper.addEventListener(
this._frameManager,
this.#frameManager,
FrameManagerEmittedEvents.LifecycleEvent,
this._checkLifecycleComplete.bind(this)
this.#checkLifecycleComplete.bind(this)
),
helper.addEventListener(
this._frameManager,
this.#frameManager,
FrameManagerEmittedEvents.FrameNavigatedWithinDocument,
this._navigatedWithinDocument.bind(this)
this.#navigatedWithinDocument.bind(this)
),
helper.addEventListener(
this._frameManager,
this.#frameManager,
FrameManagerEmittedEvents.FrameNavigated,
this._navigated.bind(this)
this.#navigated.bind(this)
),
helper.addEventListener(
this._frameManager,
this.#frameManager,
FrameManagerEmittedEvents.FrameSwapped,
this._frameSwapped.bind(this)
this.#frameSwapped.bind(this)
),
helper.addEventListener(
this._frameManager,
this.#frameManager,
FrameManagerEmittedEvents.FrameDetached,
this._onFrameDetached.bind(this)
this.#onFrameDetached.bind(this)
),
helper.addEventListener(
this._frameManager.networkManager(),
this.#frameManager.networkManager(),
NetworkManagerEmittedEvents.Request,
this._onRequest.bind(this)
this.#onRequest.bind(this)
),
];
this._timeoutPromise = this._createTimeoutPromise();
this._checkLifecycleComplete();
this.#timeoutPromise = this.#createTimeoutPromise();
this.#checkLifecycleComplete();
}
_onRequest(request: HTTPRequest): void {
if (request.frame() !== this._frame || !request.isNavigationRequest())
#onRequest(request: HTTPRequest): void {
if (request.frame() !== this.#frame || !request.isNavigationRequest())
return;
this._navigationRequest = request;
this.#navigationRequest = request;
}
_onFrameDetached(frame: Frame): void {
if (this._frame === frame) {
this._terminationCallback.call(
#onFrameDetached(frame: Frame): void {
if (this.#frame === frame) {
this.#terminationCallback.call(
null,
new Error('Navigating frame was detached')
);
return;
}
this._checkLifecycleComplete();
this.#checkLifecycleComplete();
}
async navigationResponse(): Promise<HTTPResponse | null> {
// We may need to wait for ExtraInfo events before the request is complete.
return this._navigationRequest ? this._navigationRequest.response() : null;
return this.#navigationRequest ? this.#navigationRequest.response() : null;
}
_terminate(error: Error): void {
this._terminationCallback.call(null, error);
#terminate(error: Error): void {
this.#terminationCallback.call(null, error);
}
sameDocumentNavigationPromise(): Promise<Error | undefined> {
return this._sameDocumentNavigationPromise;
return this.#sameDocumentNavigationPromise;
}
newDocumentNavigationPromise(): Promise<Error | undefined> {
return this._newDocumentNavigationPromise;
return this.#newDocumentNavigationPromise;
}
lifecyclePromise(): Promise<void> {
return this._lifecyclePromise;
return this.#lifecyclePromise;
}
timeoutOrTerminationPromise(): Promise<Error | TimeoutError | undefined> {
return Promise.race([this._timeoutPromise, this._terminationPromise]);
return Promise.race([this.#timeoutPromise, this.#terminationPromise]);
}
_createTimeoutPromise(): Promise<TimeoutError | undefined> {
if (!this._timeout) return new Promise(() => {});
async #createTimeoutPromise(): Promise<TimeoutError | undefined> {
if (!this.#timeout) return new Promise(noop);
const errorMessage =
'Navigation timeout of ' + this._timeout + ' ms exceeded';
return new Promise(
(fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout))
).then(() => new TimeoutError(errorMessage));
'Navigation timeout of ' + this.#timeout + ' ms exceeded';
await new Promise(
(fulfill) => (this.#maximumTimer = setTimeout(fulfill, this.#timeout))
);
return new TimeoutError(errorMessage);
}
_navigatedWithinDocument(frame: Frame): void {
if (frame !== this._frame) return;
this._hasSameDocumentNavigation = true;
this._checkLifecycleComplete();
#navigatedWithinDocument(frame: Frame): void {
if (frame !== this.#frame) return;
this.#hasSameDocumentNavigation = true;
this.#checkLifecycleComplete();
}
_navigated(frame: Frame): void {
if (frame !== this._frame) return;
this._newDocumentNavigation = true;
this._checkLifecycleComplete();
#navigated(frame: Frame): void {
if (frame !== this.#frame) return;
this.#newDocumentNavigation = true;
this.#checkLifecycleComplete();
}
_frameSwapped(frame: Frame): void {
if (frame !== this._frame) return;
this._swapped = true;
this._checkLifecycleComplete();
#frameSwapped(frame: Frame): void {
if (frame !== this.#frame) return;
this.#swapped = true;
this.#checkLifecycleComplete();
}
_checkLifecycleComplete(): void {
#checkLifecycleComplete(): void {
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle)) return;
this._lifecycleCallback();
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
if (this._swapped || this._newDocumentNavigation)
this._newDocumentNavigationCompleteCallback();
if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) return;
this.#lifecycleCallback();
if (this.#hasSameDocumentNavigation)
this.#sameDocumentNavigationCompleteCallback();
if (this.#swapped || this.#newDocumentNavigation)
this.#newDocumentNavigationCompleteCallback();
function checkLifecycle(
frame: Frame,
@ -256,7 +257,7 @@ export class LifecycleWatcher {
}
dispose(): void {
helper.removeEventListeners(this._eventListeners);
this._maximumTimer !== undefined && clearTimeout(this._maximumTimer);
helper.removeEventListeners(this.#eventListeners);
this.#maximumTimer !== undefined && clearTimeout(this.#maximumTimer);
}
}

View File

@ -54,15 +54,15 @@ export class NetworkEventManager {
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
* (see crbug.com/1196004)
*/
private _requestWillBeSentMap = new Map<
#requestWillBeSentMap = new Map<
NetworkRequestId,
Protocol.Network.RequestWillBeSentEvent
>();
private _requestPausedMap = new Map<
#requestPausedMap = new Map<
NetworkRequestId,
Protocol.Fetch.RequestPausedEvent
>();
private _httpRequestsMap = new Map<NetworkRequestId, HTTPRequest>();
#httpRequestsMap = new Map<NetworkRequestId, HTTPRequest>();
/*
* The below maps are used to reconcile Network.responseReceivedExtraInfo
@ -73,40 +73,37 @@ export class NetworkEventManager {
* handle redirects, we have to make them Arrays to represent the chain of
* events.
*/
private _responseReceivedExtraInfoMap = new Map<
#responseReceivedExtraInfoMap = new Map<
NetworkRequestId,
Protocol.Network.ResponseReceivedExtraInfoEvent[]
>();
private _queuedRedirectInfoMap = new Map<
NetworkRequestId,
RedirectInfoList
>();
private _queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
#queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>();
#queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
forget(networkRequestId: NetworkRequestId): void {
this._requestWillBeSentMap.delete(networkRequestId);
this._requestPausedMap.delete(networkRequestId);
this._queuedEventGroupMap.delete(networkRequestId);
this._queuedRedirectInfoMap.delete(networkRequestId);
this._responseReceivedExtraInfoMap.delete(networkRequestId);
this.#requestWillBeSentMap.delete(networkRequestId);
this.#requestPausedMap.delete(networkRequestId);
this.#queuedEventGroupMap.delete(networkRequestId);
this.#queuedRedirectInfoMap.delete(networkRequestId);
this.#responseReceivedExtraInfoMap.delete(networkRequestId);
}
responseExtraInfo(
networkRequestId: NetworkRequestId
): Protocol.Network.ResponseReceivedExtraInfoEvent[] {
if (!this._responseReceivedExtraInfoMap.has(networkRequestId)) {
this._responseReceivedExtraInfoMap.set(networkRequestId, []);
if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) {
this.#responseReceivedExtraInfoMap.set(networkRequestId, []);
}
return this._responseReceivedExtraInfoMap.get(
return this.#responseReceivedExtraInfoMap.get(
networkRequestId
) as Protocol.Network.ResponseReceivedExtraInfoEvent[];
}
private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList {
if (!this._queuedRedirectInfoMap.has(fetchRequestId)) {
this._queuedRedirectInfoMap.set(fetchRequestId, []);
if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) {
this.#queuedRedirectInfoMap.set(fetchRequestId, []);
}
return this._queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
}
queueRedirectInfo(
@ -123,7 +120,7 @@ export class NetworkEventManager {
}
numRequestsInProgress(): number {
return [...this._httpRequestsMap].filter(([, request]) => {
return [...this.#httpRequestsMap].filter(([, request]) => {
return !request.response();
}).length;
}
@ -132,62 +129,62 @@ export class NetworkEventManager {
networkRequestId: NetworkRequestId,
event: Protocol.Network.RequestWillBeSentEvent
): void {
this._requestWillBeSentMap.set(networkRequestId, event);
this.#requestWillBeSentMap.set(networkRequestId, event);
}
getRequestWillBeSent(
networkRequestId: NetworkRequestId
): Protocol.Network.RequestWillBeSentEvent | undefined {
return this._requestWillBeSentMap.get(networkRequestId);
return this.#requestWillBeSentMap.get(networkRequestId);
}
forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void {
this._requestWillBeSentMap.delete(networkRequestId);
this.#requestWillBeSentMap.delete(networkRequestId);
}
getRequestPaused(
networkRequestId: NetworkRequestId
): Protocol.Fetch.RequestPausedEvent | undefined {
return this._requestPausedMap.get(networkRequestId);
return this.#requestPausedMap.get(networkRequestId);
}
forgetRequestPaused(networkRequestId: NetworkRequestId): void {
this._requestPausedMap.delete(networkRequestId);
this.#requestPausedMap.delete(networkRequestId);
}
storeRequestPaused(
networkRequestId: NetworkRequestId,
event: Protocol.Fetch.RequestPausedEvent
): void {
this._requestPausedMap.set(networkRequestId, event);
this.#requestPausedMap.set(networkRequestId, event);
}
getRequest(networkRequestId: NetworkRequestId): HTTPRequest | undefined {
return this._httpRequestsMap.get(networkRequestId);
return this.#httpRequestsMap.get(networkRequestId);
}
storeRequest(networkRequestId: NetworkRequestId, request: HTTPRequest): void {
this._httpRequestsMap.set(networkRequestId, request);
this.#httpRequestsMap.set(networkRequestId, request);
}
forgetRequest(networkRequestId: NetworkRequestId): void {
this._httpRequestsMap.delete(networkRequestId);
this.#httpRequestsMap.delete(networkRequestId);
}
getQueuedEventGroup(
networkRequestId: NetworkRequestId
): QueuedEventGroup | undefined {
return this._queuedEventGroupMap.get(networkRequestId);
return this.#queuedEventGroupMap.get(networkRequestId);
}
queueEventGroup(
networkRequestId: NetworkRequestId,
event: QueuedEventGroup
): void {
this._queuedEventGroupMap.set(networkRequestId, event);
this.#queuedEventGroupMap.set(networkRequestId, event);
}
forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void {
this._queuedEventGroupMap.delete(networkRequestId);
this.#queuedEventGroupMap.delete(networkRequestId);
}
}

View File

@ -79,19 +79,17 @@ interface FrameManager {
* @internal
*/
export class NetworkManager extends EventEmitter {
_client: CDPSession;
_ignoreHTTPSErrors: boolean;
_frameManager: FrameManager;
_networkEventManager = new NetworkEventManager();
_extraHTTPHeaders: Record<string, string> = {};
_credentials?: Credentials;
_attemptedAuthentications = new Set<string>();
_userRequestInterceptionEnabled = false;
_protocolRequestInterceptionEnabled = false;
_userCacheDisabled = false;
_emulatedNetworkConditions: InternalNetworkConditions = {
#client: CDPSession;
#ignoreHTTPSErrors: boolean;
#frameManager: FrameManager;
#networkEventManager = new NetworkEventManager();
#extraHTTPHeaders: Record<string, string> = {};
#credentials?: Credentials;
#attemptedAuthentications = new Set<string>();
#userRequestInterceptionEnabled = false;
#protocolRequestInterceptionEnabled = false;
#userCacheDisabled = false;
#emulatedNetworkConditions: InternalNetworkConditions = {
offline: false,
upload: -1,
download: -1,
@ -104,100 +102,100 @@ export class NetworkManager extends EventEmitter {
frameManager: FrameManager
) {
super();
this._client = client;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._frameManager = frameManager;
this.#client = client;
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#frameManager = frameManager;
this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
this._client.on(
this.#client.on('Fetch.requestPaused', this.#onRequestPaused.bind(this));
this.#client.on('Fetch.authRequired', this.#onAuthRequired.bind(this));
this.#client.on(
'Network.requestWillBeSent',
this._onRequestWillBeSent.bind(this)
this.#onRequestWillBeSent.bind(this)
);
this._client.on(
this.#client.on(
'Network.requestServedFromCache',
this._onRequestServedFromCache.bind(this)
this.#onRequestServedFromCache.bind(this)
);
this._client.on(
this.#client.on(
'Network.responseReceived',
this._onResponseReceived.bind(this)
this.#onResponseReceived.bind(this)
);
this._client.on(
this.#client.on(
'Network.loadingFinished',
this._onLoadingFinished.bind(this)
this.#onLoadingFinished.bind(this)
);
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
this._client.on(
this.#client.on('Network.loadingFailed', this.#onLoadingFailed.bind(this));
this.#client.on(
'Network.responseReceivedExtraInfo',
this._onResponseReceivedExtraInfo.bind(this)
this.#onResponseReceivedExtraInfo.bind(this)
);
}
async initialize(): Promise<void> {
await this._client.send('Network.enable');
if (this._ignoreHTTPSErrors)
await this._client.send('Security.setIgnoreCertificateErrors', {
await this.#client.send('Network.enable');
if (this.#ignoreHTTPSErrors)
await this.#client.send('Security.setIgnoreCertificateErrors', {
ignore: true,
});
}
async authenticate(credentials?: Credentials): Promise<void> {
this._credentials = credentials;
await this._updateProtocolRequestInterception();
this.#credentials = credentials;
await this.#updateProtocolRequestInterception();
}
async setExtraHTTPHeaders(
extraHTTPHeaders: Record<string, string>
): Promise<void> {
this._extraHTTPHeaders = {};
this.#extraHTTPHeaders = {};
for (const key of Object.keys(extraHTTPHeaders)) {
const value = extraHTTPHeaders[key];
assert(
helper.isString(value),
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
);
this._extraHTTPHeaders[key.toLowerCase()] = value;
this.#extraHTTPHeaders[key.toLowerCase()] = value;
}
await this._client.send('Network.setExtraHTTPHeaders', {
headers: this._extraHTTPHeaders,
await this.#client.send('Network.setExtraHTTPHeaders', {
headers: this.#extraHTTPHeaders,
});
}
extraHTTPHeaders(): Record<string, string> {
return Object.assign({}, this._extraHTTPHeaders);
return Object.assign({}, this.#extraHTTPHeaders);
}
numRequestsInProgress(): number {
return this._networkEventManager.numRequestsInProgress();
return this.#networkEventManager.numRequestsInProgress();
}
async setOfflineMode(value: boolean): Promise<void> {
this._emulatedNetworkConditions.offline = value;
await this._updateNetworkConditions();
this.#emulatedNetworkConditions.offline = value;
await this.#updateNetworkConditions();
}
async emulateNetworkConditions(
networkConditions: NetworkConditions | null
): Promise<void> {
this._emulatedNetworkConditions.upload = networkConditions
this.#emulatedNetworkConditions.upload = networkConditions
? networkConditions.upload
: -1;
this._emulatedNetworkConditions.download = networkConditions
this.#emulatedNetworkConditions.download = networkConditions
? networkConditions.download
: -1;
this._emulatedNetworkConditions.latency = networkConditions
this.#emulatedNetworkConditions.latency = networkConditions
? networkConditions.latency
: 0;
await this._updateNetworkConditions();
await this.#updateNetworkConditions();
}
async _updateNetworkConditions(): Promise<void> {
await this._client.send('Network.emulateNetworkConditions', {
offline: this._emulatedNetworkConditions.offline,
latency: this._emulatedNetworkConditions.latency,
uploadThroughput: this._emulatedNetworkConditions.upload,
downloadThroughput: this._emulatedNetworkConditions.download,
async #updateNetworkConditions(): Promise<void> {
await this.#client.send('Network.emulateNetworkConditions', {
offline: this.#emulatedNetworkConditions.offline,
latency: this.#emulatedNetworkConditions.latency,
uploadThroughput: this.#emulatedNetworkConditions.upload,
downloadThroughput: this.#emulatedNetworkConditions.download,
});
}
@ -205,96 +203,96 @@ export class NetworkManager extends EventEmitter {
userAgent: string,
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
): Promise<void> {
await this._client.send('Network.setUserAgentOverride', {
await this.#client.send('Network.setUserAgentOverride', {
userAgent: userAgent,
userAgentMetadata: userAgentMetadata,
});
}
async setCacheEnabled(enabled: boolean): Promise<void> {
this._userCacheDisabled = !enabled;
await this._updateProtocolCacheDisabled();
this.#userCacheDisabled = !enabled;
await this.#updateProtocolCacheDisabled();
}
async setRequestInterception(value: boolean): Promise<void> {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
this.#userRequestInterceptionEnabled = value;
await this.#updateProtocolRequestInterception();
}
async _updateProtocolRequestInterception(): Promise<void> {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled) return;
this._protocolRequestInterceptionEnabled = enabled;
async #updateProtocolRequestInterception(): Promise<void> {
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) return;
this.#protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.enable', {
this.#updateProtocolCacheDisabled(),
this.#client.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{ urlPattern: '*' }],
}),
]);
} else {
await Promise.all([
this._updateProtocolCacheDisabled(),
this._client.send('Fetch.disable'),
this.#updateProtocolCacheDisabled(),
this.#client.send('Fetch.disable'),
]);
}
}
_cacheDisabled(): boolean {
return this._userCacheDisabled;
#cacheDisabled(): boolean {
return this.#userCacheDisabled;
}
async _updateProtocolCacheDisabled(): Promise<void> {
await this._client.send('Network.setCacheDisabled', {
cacheDisabled: this._cacheDisabled(),
async #updateProtocolCacheDisabled(): Promise<void> {
await this.#client.send('Network.setCacheDisabled', {
cacheDisabled: this.#cacheDisabled(),
});
}
_onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void {
#onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void {
// Request interception doesn't happen for data URLs with Network Service.
if (
this._userRequestInterceptionEnabled &&
this.#userRequestInterceptionEnabled &&
!event.request.url.startsWith('data:')
) {
const { requestId: networkRequestId } = event;
this._networkEventManager.storeRequestWillBeSent(networkRequestId, event);
this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
/**
* CDP may have sent a Fetch.requestPaused event already. Check for it.
*/
const requestPausedEvent =
this._networkEventManager.getRequestPaused(networkRequestId);
this.#networkEventManager.getRequestPaused(networkRequestId);
if (requestPausedEvent) {
const { requestId: fetchRequestId } = requestPausedEvent;
this._patchRequestEventHeaders(event, requestPausedEvent);
this._onRequest(event, fetchRequestId);
this._networkEventManager.forgetRequestPaused(networkRequestId);
this.#patchRequestEventHeaders(event, requestPausedEvent);
this.#onRequest(event, fetchRequestId);
this.#networkEventManager.forgetRequestPaused(networkRequestId);
}
return;
}
this._onRequest(event, undefined);
this.#onRequest(event, undefined);
}
_onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void {
#onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void {
/* TODO(jacktfranklin): This is defined in protocol.d.ts but not
* in an easily referrable way - we should look at exposing it.
*/
type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials';
let response: AuthResponse = 'Default';
if (this._attemptedAuthentications.has(event.requestId)) {
if (this.#attemptedAuthentications.has(event.requestId)) {
response = 'CancelAuth';
} else if (this._credentials) {
} else if (this.#credentials) {
response = 'ProvideCredentials';
this._attemptedAuthentications.add(event.requestId);
this.#attemptedAuthentications.add(event.requestId);
}
const { username, password } = this._credentials || {
const { username, password } = this.#credentials || {
username: undefined,
password: undefined,
};
this._client
this.#client
.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
@ -308,15 +306,13 @@ export class NetworkManager extends EventEmitter {
*
* CDP may send multiple Fetch.requestPaused
* for the same Network.requestWillBeSent.
*
*
*/
_onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void {
#onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void {
if (
!this._userRequestInterceptionEnabled &&
this._protocolRequestInterceptionEnabled
!this.#userRequestInterceptionEnabled &&
this.#protocolRequestInterceptionEnabled
) {
this._client
this.#client
.send('Fetch.continueRequest', {
requestId: event.requestId,
})
@ -331,7 +327,7 @@ export class NetworkManager extends EventEmitter {
const requestWillBeSentEvent = (() => {
const requestWillBeSentEvent =
this._networkEventManager.getRequestWillBeSent(networkRequestId);
this.#networkEventManager.getRequestWillBeSent(networkRequestId);
// redirect requests have the same `requestId`,
if (
@ -339,21 +335,21 @@ export class NetworkManager extends EventEmitter {
(requestWillBeSentEvent.request.url !== event.request.url ||
requestWillBeSentEvent.request.method !== event.request.method)
) {
this._networkEventManager.forgetRequestWillBeSent(networkRequestId);
this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
return;
}
return requestWillBeSentEvent;
})();
if (requestWillBeSentEvent) {
this._patchRequestEventHeaders(requestWillBeSentEvent, event);
this._onRequest(requestWillBeSentEvent, fetchRequestId);
this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
this.#onRequest(requestWillBeSentEvent, fetchRequestId);
} else {
this._networkEventManager.storeRequestPaused(networkRequestId, event);
this.#networkEventManager.storeRequestPaused(networkRequestId, event);
}
}
_patchRequestEventHeaders(
#patchRequestEventHeaders(
requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
requestPausedEvent: Protocol.Fetch.RequestPausedEvent
): void {
@ -364,7 +360,7 @@ export class NetworkManager extends EventEmitter {
};
}
_onRequest(
#onRequest(
event: Protocol.Network.RequestWillBeSentEvent,
fetchRequestId?: FetchRequestId
): void {
@ -379,11 +375,11 @@ export class NetworkManager extends EventEmitter {
// response/requestfinished.
let redirectResponseExtraInfo = null;
if (event.redirectHasExtraInfo) {
redirectResponseExtraInfo = this._networkEventManager
redirectResponseExtraInfo = this.#networkEventManager
.responseExtraInfo(event.requestId)
.shift();
if (!redirectResponseExtraInfo) {
this._networkEventManager.queueRedirectInfo(event.requestId, {
this.#networkEventManager.queueRedirectInfo(event.requestId, {
event,
fetchRequestId,
});
@ -391,11 +387,11 @@ export class NetworkManager extends EventEmitter {
}
}
const request = this._networkEventManager.getRequest(event.requestId);
const request = this.#networkEventManager.getRequest(event.requestId);
// If we connect late to the target, we could have missed the
// requestWillBeSent event.
if (request) {
this._handleRequestRedirect(
this.#handleRequestRedirect(
request,
event.redirectResponse,
redirectResponseExtraInfo
@ -404,37 +400,37 @@ export class NetworkManager extends EventEmitter {
}
}
const frame = event.frameId
? this._frameManager.frame(event.frameId)
? this.#frameManager.frame(event.frameId)
: null;
const request = new HTTPRequest(
this._client,
this.#client,
frame,
fetchRequestId,
this._userRequestInterceptionEnabled,
this.#userRequestInterceptionEnabled,
event,
redirectChain
);
this._networkEventManager.storeRequest(event.requestId, request);
this.#networkEventManager.storeRequest(event.requestId, request);
this.emit(NetworkManagerEmittedEvents.Request, request);
request.finalizeInterceptions();
}
_onRequestServedFromCache(
#onRequestServedFromCache(
event: Protocol.Network.RequestServedFromCacheEvent
): void {
const request = this._networkEventManager.getRequest(event.requestId);
const request = this.#networkEventManager.getRequest(event.requestId);
if (request) request._fromMemoryCache = true;
this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request);
}
_handleRequestRedirect(
#handleRequestRedirect(
request: HTTPRequest,
responsePayload: Protocol.Network.Response,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): void {
const response = new HTTPResponse(
this._client,
this.#client,
request,
responsePayload,
extraInfo
@ -444,22 +440,22 @@ export class NetworkManager extends EventEmitter {
response._resolveBody(
new Error('Response body is unavailable for redirect responses')
);
this._forgetRequest(request, false);
this.#forgetRequest(request, false);
this.emit(NetworkManagerEmittedEvents.Response, response);
this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
}
_emitResponseEvent(
#emitResponseEvent(
responseReceived: Protocol.Network.ResponseReceivedEvent,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): void {
const request = this._networkEventManager.getRequest(
const request = this.#networkEventManager.getRequest(
responseReceived.requestId
);
// FileUpload sends a response without a matching request.
if (!request) return;
const extraInfos = this._networkEventManager.responseExtraInfo(
const extraInfos = this.#networkEventManager.responseExtraInfo(
responseReceived.requestId
);
if (extraInfos.length) {
@ -472,7 +468,7 @@ export class NetworkManager extends EventEmitter {
}
const response = new HTTPResponse(
this._client,
this.#client,
request,
responseReceived.response,
extraInfo
@ -481,88 +477,88 @@ export class NetworkManager extends EventEmitter {
this.emit(NetworkManagerEmittedEvents.Response, response);
}
_onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void {
const request = this._networkEventManager.getRequest(event.requestId);
#onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void {
const request = this.#networkEventManager.getRequest(event.requestId);
let extraInfo = null;
if (request && !request._fromMemoryCache && event.hasExtraInfo) {
extraInfo = this._networkEventManager
extraInfo = this.#networkEventManager
.responseExtraInfo(event.requestId)
.shift();
if (!extraInfo) {
// Wait until we get the corresponding ExtraInfo event.
this._networkEventManager.queueEventGroup(event.requestId, {
this.#networkEventManager.queueEventGroup(event.requestId, {
responseReceivedEvent: event,
});
return;
}
}
this._emitResponseEvent(event, extraInfo);
this.#emitResponseEvent(event, extraInfo);
}
_onResponseReceivedExtraInfo(
#onResponseReceivedExtraInfo(
event: Protocol.Network.ResponseReceivedExtraInfoEvent
): void {
// We may have skipped a redirect response/request pair due to waiting for
// this ExtraInfo event. If so, continue that work now that we have the
// request.
const redirectInfo = this._networkEventManager.takeQueuedRedirectInfo(
const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
event.requestId
);
if (redirectInfo) {
this._networkEventManager.responseExtraInfo(event.requestId).push(event);
this._onRequest(redirectInfo.event, redirectInfo.fetchRequestId);
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
this.#onRequest(redirectInfo.event, redirectInfo.fetchRequestId);
return;
}
// We may have skipped response and loading events because we didn't have
// this ExtraInfo event yet. If so, emit those events now.
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId
);
if (queuedEvents) {
this._networkEventManager.forgetQueuedEventGroup(event.requestId);
this._emitResponseEvent(queuedEvents.responseReceivedEvent, event);
this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
this.#emitResponseEvent(queuedEvents.responseReceivedEvent, event);
if (queuedEvents.loadingFinishedEvent) {
this._emitLoadingFinished(queuedEvents.loadingFinishedEvent);
this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent);
}
if (queuedEvents.loadingFailedEvent) {
this._emitLoadingFailed(queuedEvents.loadingFailedEvent);
this.#emitLoadingFailed(queuedEvents.loadingFailedEvent);
}
return;
}
// Wait until we get another event that can use this ExtraInfo event.
this._networkEventManager.responseExtraInfo(event.requestId).push(event);
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
}
_forgetRequest(request: HTTPRequest, events: boolean): void {
#forgetRequest(request: HTTPRequest, events: boolean): void {
const requestId = request._requestId;
const interceptionId = request._interceptionId;
this._networkEventManager.forgetRequest(requestId);
this.#networkEventManager.forgetRequest(requestId);
interceptionId !== undefined &&
this._attemptedAuthentications.delete(interceptionId);
this.#attemptedAuthentications.delete(interceptionId);
if (events) {
this._networkEventManager.forget(requestId);
this.#networkEventManager.forget(requestId);
}
}
_onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
#onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
// If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId
);
if (queuedEvents) {
queuedEvents.loadingFinishedEvent = event;
} else {
this._emitLoadingFinished(event);
this.#emitLoadingFinished(event);
}
}
_emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
const request = this._networkEventManager.getRequest(event.requestId);
#emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) return;
@ -570,32 +566,32 @@ export class NetworkManager extends EventEmitter {
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
if (request.response()) request.response()?._resolveBody(null);
this._forgetRequest(request, true);
this.#forgetRequest(request, true);
this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
}
_onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
#onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
// If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId
);
if (queuedEvents) {
queuedEvents.loadingFailedEvent = event;
} else {
this._emitLoadingFailed(event);
this.#emitLoadingFailed(event);
}
}
_emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
const request = this._networkEventManager.getRequest(event.requestId);
#emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) return;
request._failureText = event.errorText;
const response = request.response();
if (response) response._resolveBody(null);
this._forgetRequest(request, true);
this.#forgetRequest(request, true);
this.emit(NetworkManagerEmittedEvents.RequestFailed, request);
}
}

View File

@ -182,17 +182,19 @@ export interface PaperFormatDimensions {
/**
* @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;
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;

File diff suppressed because it is too large Load Diff

View File

@ -15,17 +15,20 @@
*/
import { puppeteerErrors, PuppeteerErrors } from './Errors.js';
import { ConnectionTransport } from './ConnectionTransport.js';
import { devicesMap, DevicesMap } from './DeviceDescriptors.js';
import { _devicesMap, DevicesMap } from './DeviceDescriptors.js';
import { Browser } from './Browser.js';
import {
registerCustomQueryHandler,
unregisterCustomQueryHandler,
customQueryHandlerNames,
clearCustomQueryHandlers,
_registerCustomQueryHandler,
_unregisterCustomQueryHandler,
_customQueryHandlerNames,
_clearCustomQueryHandlers,
CustomQueryHandler,
} from './QueryHandler.js';
import { Product } from './Product.js';
import { connectToBrowser, BrowserConnectOptions } from './BrowserConnector.js';
import {
_connectToBrowser,
BrowserConnectOptions,
} from './BrowserConnector.js';
import {
PredefinedNetworkConditions,
networkConditions,
@ -33,6 +36,7 @@ import {
/**
* Settings that are common to the Puppeteer class, regardless of environment.
*
* @internal
*/
export interface CommonPuppeteerSettings {
@ -85,7 +89,7 @@ export class Puppeteer {
* @returns Promise which resolves to browser instance.
*/
connect(options: ConnectOptions): Promise<Browser> {
return connectToBrowser(options);
return _connectToBrowser(options);
}
/**
@ -110,7 +114,7 @@ export class Puppeteer {
*
*/
get devices(): DevicesMap {
return devicesMap;
return _devicesMap;
}
/**
@ -182,27 +186,27 @@ export class Puppeteer {
name: string,
queryHandler: CustomQueryHandler
): void {
registerCustomQueryHandler(name, queryHandler);
_registerCustomQueryHandler(name, queryHandler);
}
/**
* @param name - The name of the query handler to unregistered.
*/
unregisterCustomQueryHandler(name: string): void {
unregisterCustomQueryHandler(name);
_unregisterCustomQueryHandler(name);
}
/**
* @returns a list with the names of all registered custom query handlers.
*/
customQueryHandlerNames(): string[] {
return customQueryHandlerNames();
return _customQueryHandlerNames();
}
/**
* Clears all registered handlers.
*/
clearCustomQueryHandlers(): void {
clearCustomQueryHandlers();
_clearCustomQueryHandlers();
}
}

View File

@ -16,7 +16,7 @@
import { WaitForSelectorOptions, DOMWorld } from './DOMWorld.js';
import { ElementHandle, JSHandle } from './JSHandle.js';
import { ariaHandler } from './AriaQueryHandler.js';
import { _ariaHandler } from './AriaQueryHandler.js';
/**
* @internal
@ -76,7 +76,7 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
domWorld: DOMWorld,
selector: string,
options: WaitForSelectorOptions
) => domWorld.waitForSelectorInPage(queryOne, selector, options);
) => domWorld._waitForSelectorInPage(queryOne, selector, options);
}
if (handler.queryAll) {
@ -161,20 +161,20 @@ const pierceHandler = makeQueryHandler({
},
});
const _builtInHandlers = new Map([
['aria', ariaHandler],
const builtInHandlers = new Map([
['aria', _ariaHandler],
['pierce', pierceHandler],
]);
const _queryHandlers = new Map(_builtInHandlers);
const queryHandlers = new Map(builtInHandlers);
/**
* @internal
*/
export function registerCustomQueryHandler(
export function _registerCustomQueryHandler(
name: string,
handler: CustomQueryHandler
): void {
if (_queryHandlers.get(name))
if (queryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`);
const isValidName = /^[a-zA-Z]+$/.test(name);
@ -183,38 +183,36 @@ export function registerCustomQueryHandler(
const internalHandler = makeQueryHandler(handler);
_queryHandlers.set(name, internalHandler);
queryHandlers.set(name, internalHandler);
}
/**
* @internal
*/
export function unregisterCustomQueryHandler(name: string): void {
if (_queryHandlers.has(name) && !_builtInHandlers.has(name)) {
_queryHandlers.delete(name);
export function _unregisterCustomQueryHandler(name: string): void {
if (queryHandlers.has(name) && !builtInHandlers.has(name)) {
queryHandlers.delete(name);
}
}
/**
* @internal
*/
export function customQueryHandlerNames(): string[] {
return [..._queryHandlers.keys()].filter(
(name) => !_builtInHandlers.has(name)
);
export function _customQueryHandlerNames(): string[] {
return [...queryHandlers.keys()].filter((name) => !builtInHandlers.has(name));
}
/**
* @internal
*/
export function clearCustomQueryHandlers(): void {
customQueryHandlerNames().forEach(unregisterCustomQueryHandler);
export function _clearCustomQueryHandlers(): void {
_customQueryHandlerNames().forEach(_unregisterCustomQueryHandler);
}
/**
* @internal
*/
export function getQueryHandlerAndSelector(selector: string): {
export function _getQueryHandlerAndSelector(selector: string): {
updatedSelector: string;
queryHandler: InternalQueryHandler;
} {
@ -225,7 +223,7 @@ export function getQueryHandlerAndSelector(selector: string): {
const index = selector.indexOf('/');
const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1);
const queryHandler = _queryHandlers.get(name);
const queryHandler = queryHandlers.get(name);
if (!queryHandler)
throw new Error(
`Query set to use "${name}", but no query handler of that name was found`

View File

@ -23,30 +23,30 @@ import { Protocol } from 'devtools-protocol';
* @public
*/
export class SecurityDetails {
private _subjectName: string;
private _issuer: string;
private _validFrom: number;
private _validTo: number;
private _protocol: string;
private _sanList: string[];
#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;
this.#subjectName = securityPayload.subjectName;
this.#issuer = securityPayload.issuer;
this.#validFrom = securityPayload.validFrom;
this.#validTo = securityPayload.validTo;
this.#protocol = securityPayload.protocol;
this.#sanList = securityPayload.sanList;
}
/**
* @returns The name of the issuer of the certificate.
*/
issuer(): string {
return this._issuer;
return this.#issuer;
}
/**
@ -54,7 +54,7 @@ export class SecurityDetails {
* marking the start of the certificate's validity.
*/
validFrom(): number {
return this._validFrom;
return this.#validFrom;
}
/**
@ -62,27 +62,27 @@ export class SecurityDetails {
* marking the end of the certificate's validity.
*/
validTo(): number {
return this._validTo;
return this.#validTo;
}
/**
* @returns The security protocol being used, e.g. "TLS 1.2".
*/
protocol(): string {
return this._protocol;
return this.#protocol;
}
/**
* @returns The name of the subject to which the certificate was issued.
*/
subjectName(): string {
return this._subjectName;
return this.#subjectName;
}
/**
* @returns The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate.
*/
subjectAlternativeNames(): string[] {
return this._sanList;
return this.#sanList;
}
}

View File

@ -26,15 +26,15 @@ import { TaskQueue } from './TaskQueue.js';
* @public
*/
export class Target {
private _targetInfo: Protocol.Target.TargetInfo;
private _browserContext: BrowserContext;
#browserContext: BrowserContext;
#targetInfo: Protocol.Target.TargetInfo;
#sessionFactory: () => Promise<CDPSession>;
#ignoreHTTPSErrors: boolean;
#defaultViewport?: Viewport;
#pagePromise?: Promise<Page>;
#workerPromise?: Promise<WebWorker>;
#screenshotTaskQueue: TaskQueue;
private _sessionFactory: () => Promise<CDPSession>;
private _ignoreHTTPSErrors: boolean;
private _defaultViewport?: Viewport;
private _pagePromise?: Promise<Page>;
private _workerPromise?: Promise<WebWorker>;
private _screenshotTaskQueue: TaskQueue;
/**
* @internal
*/
@ -76,22 +76,22 @@ export class Target {
screenshotTaskQueue: TaskQueue,
isPageTargetCallback: IsPageTargetCallback
) {
this._targetInfo = targetInfo;
this._browserContext = browserContext;
this.#targetInfo = targetInfo;
this.#browserContext = browserContext;
this._targetId = targetInfo.targetId;
this._sessionFactory = sessionFactory;
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport ?? undefined;
this._screenshotTaskQueue = screenshotTaskQueue;
this.#sessionFactory = sessionFactory;
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport ?? undefined;
this.#screenshotTaskQueue = screenshotTaskQueue;
this._isPageTargetCallback = isPageTargetCallback;
this._initializedPromise = new Promise<boolean>(
(fulfill) => (this._initializedCallback = fulfill)
).then(async (success) => {
if (!success) return false;
const opener = this.opener();
if (!opener || !opener._pagePromise || this.type() !== 'page')
if (!opener || !opener.#pagePromise || this.type() !== 'page')
return true;
const openerPage = await opener._pagePromise;
const openerPage = await opener.#pagePromise;
if (!openerPage.listenerCount(PageEmittedEvents.Popup)) return true;
const popupPage = await this.page();
openerPage.emit(PageEmittedEvents.Popup, popupPage);
@ -101,8 +101,8 @@ export class Target {
(fulfill) => (this._closedCallback = fulfill)
);
this._isInitialized =
!this._isPageTargetCallback(this._targetInfo) ||
this._targetInfo.url !== '';
!this._isPageTargetCallback(this.#targetInfo) ||
this.#targetInfo.url !== '';
if (this._isInitialized) this._initializedCallback(true);
}
@ -110,32 +110,32 @@ export class Target {
* Creates a Chrome Devtools Protocol session attached to the target.
*/
createCDPSession(): Promise<CDPSession> {
return this._sessionFactory();
return this.#sessionFactory();
}
/**
* @internal
*/
_getTargetInfo(): Protocol.Target.TargetInfo {
return this._targetInfo;
return this.#targetInfo;
}
/**
* If the target is not of type `"page"` or `"background_page"`, returns `null`.
*/
async page(): Promise<Page | null> {
if (this._isPageTargetCallback(this._targetInfo) && !this._pagePromise) {
this._pagePromise = this._sessionFactory().then((client) =>
Page.create(
if (this._isPageTargetCallback(this.#targetInfo) && !this.#pagePromise) {
this.#pagePromise = this.#sessionFactory().then((client) =>
Page._create(
client,
this,
this._ignoreHTTPSErrors,
this._defaultViewport ?? null,
this._screenshotTaskQueue
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null,
this.#screenshotTaskQueue
)
);
}
return (await this._pagePromise) ?? null;
return (await this.#pagePromise) ?? null;
}
/**
@ -143,27 +143,27 @@ export class Target {
*/
async worker(): Promise<WebWorker | null> {
if (
this._targetInfo.type !== 'service_worker' &&
this._targetInfo.type !== 'shared_worker'
this.#targetInfo.type !== 'service_worker' &&
this.#targetInfo.type !== 'shared_worker'
)
return null;
if (!this._workerPromise) {
if (!this.#workerPromise) {
// TODO(einbinder): Make workers send their console logs.
this._workerPromise = this._sessionFactory().then(
this.#workerPromise = this.#sessionFactory().then(
(client) =>
new WebWorker(
client,
this._targetInfo.url,
this.#targetInfo.url,
() => {} /* consoleAPICalled */,
() => {} /* exceptionThrown */
)
);
}
return this._workerPromise;
return this.#workerPromise;
}
url(): string {
return this._targetInfo.url;
return this.#targetInfo.url;
}
/**
@ -181,7 +181,7 @@ export class Target {
| 'other'
| 'browser'
| 'webview' {
const type = this._targetInfo.type;
const type = this.#targetInfo.type;
if (
type === 'page' ||
type === 'background_page' ||
@ -198,21 +198,21 @@ export class Target {
* Get the browser the target belongs to.
*/
browser(): Browser {
return this._browserContext.browser();
return this.#browserContext.browser();
}
/**
* Get the browser context the target belongs to.
*/
browserContext(): BrowserContext {
return this._browserContext;
return this.#browserContext;
}
/**
* Get the target that opened this target. Top-level targets return `null`.
*/
opener(): Target | undefined {
const { openerId } = this._targetInfo;
const { openerId } = this.#targetInfo;
if (!openerId) return;
return this.browser()._targets.get(openerId);
}
@ -221,12 +221,12 @@ export class Target {
* @internal
*/
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
this._targetInfo = targetInfo;
this.#targetInfo = targetInfo;
if (
!this._isInitialized &&
(!this._isPageTargetCallback(this._targetInfo) ||
this._targetInfo.url !== '')
(!this._isPageTargetCallback(this.#targetInfo) ||
this.#targetInfo.url !== '')
) {
this._isInitialized = true;
this._initializedCallback(true);

View File

@ -15,15 +15,15 @@
*/
export class TaskQueue {
private _chain: Promise<void>;
#chain: Promise<void>;
constructor() {
this._chain = Promise.resolve();
this.#chain = Promise.resolve();
}
postTask<T>(task: () => Promise<T>): Promise<T> {
const result = this._chain.then(task);
this._chain = result.then(
const result = this.#chain.then(task);
this.#chain = result.then(
() => undefined,
() => undefined
);

View File

@ -20,31 +20,31 @@ const DEFAULT_TIMEOUT = 30000;
* @internal
*/
export class TimeoutSettings {
_defaultTimeout: number | null;
_defaultNavigationTimeout: number | null;
#defaultTimeout: number | null;
#defaultNavigationTimeout: number | null;
constructor() {
this._defaultTimeout = null;
this._defaultNavigationTimeout = null;
this.#defaultTimeout = null;
this.#defaultNavigationTimeout = null;
}
setDefaultTimeout(timeout: number): void {
this._defaultTimeout = timeout;
this.#defaultTimeout = timeout;
}
setDefaultNavigationTimeout(timeout: number): void {
this._defaultNavigationTimeout = timeout;
this.#defaultNavigationTimeout = timeout;
}
navigationTimeout(): number {
if (this._defaultNavigationTimeout !== null)
return this._defaultNavigationTimeout;
if (this._defaultTimeout !== null) return this._defaultTimeout;
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;
if (this.#defaultTimeout !== null) return this.#defaultTimeout;
return DEFAULT_TIMEOUT;
}
}

View File

@ -42,26 +42,27 @@ export interface TracingOptions {
* @public
*/
export class Tracing {
_client: CDPSession;
_recording = false;
_path?: string;
#client: CDPSession;
#recording = false;
#path?: string;
/**
* @internal
*/
constructor(client: CDPSession) {
this._client = client;
this.#client = client;
}
/**
* Starts a trace for the current page.
* @remarks
* Only one trace can be active at a time per browser.
*
* @param options - Optional `TracingOptions`.
*/
async start(options: TracingOptions = {}): Promise<void> {
assert(
!this._recording,
!this.#recording,
'Cannot start recording trace while already recording trace.'
);
@ -91,9 +92,9 @@ export class Tracing {
.map((cat) => cat.slice(1));
const includedCategories = categories.filter((cat) => !cat.startsWith('-'));
this._path = path;
this._recording = true;
await this._client.send('Tracing.start', {
this.#path = path;
this.#recording = true;
await this.#client.send('Tracing.start', {
transferMode: 'ReturnAsStream',
traceConfig: {
excludedCategories,
@ -113,13 +114,13 @@ export class Tracing {
resolve = x;
reject = y;
});
this._client.once('Tracing.tracingComplete', async (event) => {
this.#client.once('Tracing.tracingComplete', async (event) => {
try {
const readable = await helper.getReadableFromProtocolStream(
this._client,
this.#client,
event.stream
);
const buffer = await helper.getReadableAsBuffer(readable, this._path);
const buffer = await helper.getReadableAsBuffer(readable, this.#path);
resolve(buffer ?? undefined);
} catch (error) {
if (isErrorLike(error)) {
@ -129,8 +130,8 @@ export class Tracing {
}
}
});
await this._client.send('Tracing.end');
this._recording = false;
await this.#client.send('Tracing.end');
this.#recording = false;
return contentPromise;
}
}

View File

@ -294,7 +294,7 @@ export type KeyInput =
/**
* @internal
*/
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
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' },

View File

@ -61,10 +61,10 @@ type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
* @public
*/
export class WebWorker extends EventEmitter {
_client: CDPSession;
_url: string;
_executionContextPromise: Promise<ExecutionContext>;
_executionContextCallback!: (value: ExecutionContext) => void;
#client: CDPSession;
#url: string;
#executionContextPromise: Promise<ExecutionContext>;
#executionContextCallback!: (value: ExecutionContext) => void;
/**
*
@ -77,31 +77,31 @@ export class WebWorker extends EventEmitter {
exceptionThrown: ExceptionThrownCallback
) {
super();
this._client = client;
this._url = url;
this._executionContextPromise = new Promise<ExecutionContext>(
(x) => (this._executionContextCallback = x)
this.#client = client;
this.#url = url;
this.#executionContextPromise = new Promise<ExecutionContext>(
(x) => (this.#executionContextCallback = x)
);
let jsHandleFactory: JSHandleFactory;
this._client.once('Runtime.executionContextCreated', async (event) => {
this.#client.once('Runtime.executionContextCreated', async (event) => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
jsHandleFactory = (remoteObject) =>
new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(client, event.context);
this._executionContextCallback(executionContext);
this.#executionContextCallback(executionContext);
});
// This might fail if the target is closed before we receive all execution contexts.
this._client.send('Runtime.enable').catch(debugError);
this._client.on('Runtime.consoleAPICalled', (event) =>
this.#client.send('Runtime.enable').catch(debugError);
this.#client.on('Runtime.consoleAPICalled', (event) =>
consoleAPICalled(
event.type,
event.args.map(jsHandleFactory),
event.stackTrace
)
);
this._client.on('Runtime.exceptionThrown', (exception) =>
this.#client.on('Runtime.exceptionThrown', (exception) =>
exceptionThrown(exception.exceptionDetails)
);
}
@ -110,7 +110,7 @@ export class WebWorker extends EventEmitter {
* @returns The URL of this web worker.
*/
url(): string {
return this._url;
return this.#url;
}
/**
@ -118,7 +118,7 @@ export class WebWorker extends EventEmitter {
* @returns The ExecutionContext the web worker runs in.
*/
async executionContext(): Promise<ExecutionContext> {
return this._executionContextPromise;
return this.#executionContextPromise;
}
/**
@ -139,7 +139,7 @@ export class WebWorker extends EventEmitter {
pageFunction: Function | string,
...args: any[]
): Promise<ReturnType> {
return (await this._executionContextPromise).evaluate<ReturnType>(
return (await this.#executionContextPromise).evaluate<ReturnType>(
pageFunction,
...args
);
@ -161,7 +161,7 @@ export class WebWorker extends EventEmitter {
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<JSHandle> {
return (await this._executionContextPromise).evaluateHandle<HandlerType>(
return (await this.#executionContextPromise).evaluateHandle<HandlerType>(
pageFunction,
...args
);

View File

@ -103,7 +103,7 @@ function archiveName(
/**
* @internal
*/
function downloadURL(
function _downloadURL(
product: Product,
platform: Platform,
host: string,
@ -118,26 +118,24 @@ function downloadURL(
return url;
}
/**
* @internal
*/
function handleArm64(): void {
fs.stat('/usr/bin/chromium-browser', function (_err, stats) {
if (stats === undefined) {
fs.stat('/usr/bin/chromium', function (_err, stats) {
if (stats === undefined) {
console.error(
'The chromium binary is not available for arm64.' +
'\nIf you are on Ubuntu, you can install with: ' +
'\n\n sudo apt install chromium\n' +
'\n\n sudo apt install chromium-browser\n'
);
throw new Error();
}
});
}
});
let exists = fs.existsSync('/usr/bin/chromium-browser');
if (exists) {
return;
}
exists = fs.existsSync('/usr/bin/chromium');
if (exists) {
return;
}
console.error(
'The chromium binary is not available for arm64.' +
'\nIf you are on Ubuntu, you can install with: ' +
'\n\n sudo apt install chromium\n' +
'\n\n sudo apt install chromium-browser\n'
);
throw new Error();
}
const readdirAsync = promisify(fs.readdir.bind(fs));
const mkdirAsync = promisify(fs.mkdir.bind(fs));
const unlinkAsync = promisify(fs.unlink.bind(fs));
@ -195,49 +193,49 @@ export interface BrowserFetcherRevisionInfo {
*/
export class BrowserFetcher {
private _product: Product;
private _downloadsFolder: string;
private _downloadHost: string;
private _platform: Platform;
#product: Product;
#downloadsFolder: string;
#downloadHost: string;
#platform: Platform;
/**
* @internal
*/
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
this._product = (options.product || 'chrome').toLowerCase() as Product;
this.#product = (options.product || 'chrome').toLowerCase() as Product;
assert(
this._product === 'chrome' || this._product === 'firefox',
this.#product === 'chrome' || this.#product === 'firefox',
`Unknown product: "${options.product}"`
);
this._downloadsFolder =
this.#downloadsFolder =
options.path ||
path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host;
path.join(projectRoot, browserConfig[this.#product].destination);
this.#downloadHost = options.host || browserConfig[this.#product].host;
if (options.platform) {
this._platform = options.platform;
this.#platform = options.platform;
} else {
const platform = os.platform();
switch (platform) {
case 'darwin':
switch (this._product) {
switch (this.#product) {
case 'chrome':
this._platform =
this.#platform =
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
? 'mac_arm'
: 'mac';
break;
case 'firefox':
this._platform = 'mac';
this.#platform = 'mac';
break;
}
break;
case 'linux':
this._platform = 'linux';
this.#platform = 'linux';
break;
case 'win32':
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
this.#platform = os.arch() === 'x64' ? 'win64' : 'win32';
return;
default:
assert(false, 'Unsupported platform: ' + platform);
@ -245,8 +243,8 @@ export class BrowserFetcher {
}
assert(
downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform
downloadURLs[this.#product][this.#platform],
'Unsupported platform: ' + this.#platform
);
}
@ -255,7 +253,7 @@ export class BrowserFetcher {
* `win32` or `win64`.
*/
platform(): Platform {
return this._platform;
return this.#platform;
}
/**
@ -263,14 +261,14 @@ export class BrowserFetcher {
* `firefox`.
*/
product(): Product {
return this._product;
return this.#product;
}
/**
* @returns The download host being used.
*/
host(): string {
return this._downloadHost;
return this.#downloadHost;
}
/**
@ -282,10 +280,10 @@ export class BrowserFetcher {
* from the host.
*/
canDownload(revision: string): Promise<boolean> {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
const url = _downloadURL(
this.#product,
this.#platform,
this.#downloadHost,
revision
);
return new Promise((resolve) => {
@ -318,19 +316,19 @@ export class BrowserFetcher {
revision: string,
progressCallback: (x: number, y: number) => void = (): void => {}
): Promise<BrowserFetcherRevisionInfo | undefined> {
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
const url = _downloadURL(
this.#product,
this.#platform,
this.#downloadHost,
revision
);
const fileName = url.split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision);
const archivePath = path.join(this.#downloadsFolder, fileName);
const outputPath = this.#getFolderPath(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder)))
await mkdirAsync(this._downloadsFolder);
if (!(await existsAsync(this.#downloadsFolder)))
await mkdirAsync(this.#downloadsFolder);
// Use system Chromium builds on Linux ARM devices
if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
@ -338,7 +336,7 @@ export class BrowserFetcher {
return;
}
try {
await downloadFile(url, archivePath, progressCallback);
await _downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath);
} finally {
if (await existsAsync(archivePath)) await unlinkAsync(archivePath);
@ -355,15 +353,15 @@ export class BrowserFetcher {
* available locally on disk.
*/
async localRevisions(): Promise<string[]> {
if (!(await existsAsync(this._downloadsFolder))) return [];
const fileNames = await readdirAsync(this._downloadsFolder);
if (!(await existsAsync(this.#downloadsFolder))) return [];
const fileNames = await readdirAsync(this.#downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(this._product, fileName))
.map((fileName) => parseFolderPath(this.#product, fileName))
.filter(
(
entry
): entry is { product: string; platform: string; revision: string } =>
(entry && entry.platform === this._platform) ?? false
(entry && entry.platform === this.#platform) ?? false
)
.map((entry) => entry.revision);
}
@ -376,7 +374,7 @@ export class BrowserFetcher {
* throws if the revision has not been downloaded.
*/
async remove(revision: string): Promise<void> {
const folderPath = this._getFolderPath(revision);
const folderPath = this.#getFolderPath(revision);
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`
@ -389,33 +387,33 @@ export class BrowserFetcher {
* @returns The revision info for the given revision.
*/
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
const folderPath = this._getFolderPath(revision);
const folderPath = this.#getFolderPath(revision);
let executablePath = '';
if (this._product === 'chrome') {
if (this._platform === 'mac' || this._platform === 'mac_arm')
if (this.#product === 'chrome') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
archiveName(this.#product, this.#platform, revision),
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
else if (this._platform === 'linux')
else if (this.#platform === 'linux')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
archiveName(this.#product, this.#platform, revision),
'chrome'
);
else if (this._platform === 'win32' || this._platform === 'win64')
else if (this.#platform === 'win32' || this.#platform === 'win64')
executablePath = path.join(
folderPath,
archiveName(this._product, this._platform, revision),
archiveName(this.#product, this.#platform, revision),
'chrome.exe'
);
else throw new Error('Unsupported platform: ' + this._platform);
} else if (this._product === 'firefox') {
if (this._platform === 'mac' || this._platform === 'mac_arm')
else throw new Error('Unsupported platform: ' + this.#platform);
} else if (this.#product === 'firefox') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm')
executablePath = path.join(
folderPath,
'Firefox Nightly.app',
@ -423,16 +421,16 @@ export class BrowserFetcher {
'MacOS',
'firefox'
);
else if (this._platform === 'linux')
else if (this.#platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this._platform === 'win32' || this._platform === 'win64')
else if (this.#platform === 'win32' || this.#platform === 'win64')
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else throw new Error('Unsupported platform: ' + this._platform);
} else throw new Error('Unsupported product: ' + this._product);
const url = downloadURL(
this._product,
this._platform,
this._downloadHost,
else throw new Error('Unsupported platform: ' + this.#platform);
} else throw new Error('Unsupported product: ' + this.#product);
const url = _downloadURL(
this.#product,
this.#platform,
this.#downloadHost,
revision
);
const local = fs.existsSync(folderPath);
@ -442,7 +440,7 @@ export class BrowserFetcher {
folderPath,
local,
url,
product: this._product,
product: this.#product,
});
return {
revision,
@ -450,15 +448,12 @@ export class BrowserFetcher {
folderPath,
local,
url,
product: this._product,
product: this.#product,
};
}
/**
* @internal
*/
_getFolderPath(revision: string): string {
return path.resolve(this._downloadsFolder, `${this._platform}-${revision}`);
#getFolderPath(revision: string): string {
return path.resolve(this.#downloadsFolder, `${this.#platform}-${revision}`);
}
}
@ -477,7 +472,7 @@ function parseFolderPath(
/**
* @internal
*/
function downloadFile(
function _downloadFile(
url: string,
destinationPath: string,
progressCallback?: (x: number, y: number) => void
@ -524,10 +519,10 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
if (archivePath.endsWith('.zip'))
return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2'))
return extractTar(archivePath, folderPath);
return _extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg'))
return mkdirAsync(folderPath).then(() =>
installDMG(archivePath, folderPath)
_installDMG(archivePath, folderPath)
);
else throw new Error(`Unsupported archive format: ${archivePath}`);
}
@ -535,7 +530,7 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
/**
* @internal
*/
function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
function _extractTar(tarPath: string, folderPath: string): Promise<unknown> {
return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath);
tarStream.on('error', reject);
@ -548,7 +543,7 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
/**
* @internal
*/
function installDMG(dmgPath: string, folderPath: string): Promise<void> {
function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
let mountPath: string | undefined;
return new Promise<void>((fulfill, reject): void => {

View File

@ -50,19 +50,18 @@ Please check your open processes and ensure that the browser processes that Pupp
If you think this is a bug, please report it on the Puppeteer issue tracker.`;
export class BrowserRunner {
private _product: Product;
private _executablePath: string;
private _processArguments: string[];
private _userDataDir: string;
private _isTempUserDataDir?: boolean;
#product: Product;
#executablePath: string;
#processArguments: string[];
#userDataDir: string;
#isTempUserDataDir?: boolean;
#closed = true;
#listeners: PuppeteerEventListener[] = [];
#processClosing!: Promise<void>;
proc?: childProcess.ChildProcess;
connection?: Connection;
private _closed = true;
private _listeners: PuppeteerEventListener[] = [];
private _processClosing!: Promise<void>;
constructor(
product: Product,
executablePath: string,
@ -70,11 +69,11 @@ export class BrowserRunner {
userDataDir: string,
isTempUserDataDir?: boolean
) {
this._product = product;
this._executablePath = executablePath;
this._processArguments = processArguments;
this._userDataDir = userDataDir;
this._isTempUserDataDir = isTempUserDataDir;
this.#product = product;
this.#executablePath = executablePath;
this.#processArguments = processArguments;
this.#userDataDir = userDataDir;
this.#isTempUserDataDir = isTempUserDataDir;
}
start(options: LaunchOptions): void {
@ -90,11 +89,11 @@ export class BrowserRunner {
}
assert(!this.proc, 'This process has previously been started.');
debugLauncher(
`Calling ${this._executablePath} ${this._processArguments.join(' ')}`
`Calling ${this.#executablePath} ${this.#processArguments.join(' ')}`
);
this.proc = childProcess.spawn(
this._executablePath,
this._processArguments,
this.#executablePath,
this.#processArguments,
{
// On non-windows platforms, `detached: true` makes child process a
// leader of a new process group, making it possible to kill child
@ -109,32 +108,32 @@ export class BrowserRunner {
this.proc.stderr?.pipe(process.stderr);
this.proc.stdout?.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill, reject) => {
this.#closed = false;
this.#processClosing = new Promise((fulfill, reject) => {
this.proc!.once('exit', async () => {
this._closed = true;
this.#closed = true;
// Cleanup as processes exit.
if (this._isTempUserDataDir) {
if (this.#isTempUserDataDir) {
try {
await removeFolderAsync(this._userDataDir);
await removeFolderAsync(this.#userDataDir);
fulfill();
} catch (error) {
debugError(error);
reject(error);
}
} else {
if (this._product === 'firefox') {
if (this.#product === 'firefox') {
try {
// When an existing user profile has been used remove the user
// preferences file and restore possibly backuped preferences.
await unlinkAsync(path.join(this._userDataDir, 'user.js'));
await unlinkAsync(path.join(this.#userDataDir, 'user.js'));
const prefsBackupPath = path.join(
this._userDataDir,
this.#userDataDir,
'prefs.js.puppeteer'
);
if (fs.existsSync(prefsBackupPath)) {
const prefsPath = path.join(this._userDataDir, 'prefs.js');
const prefsPath = path.join(this.#userDataDir, 'prefs.js');
await unlinkAsync(prefsPath);
await renameAsync(prefsBackupPath, prefsPath);
}
@ -148,29 +147,29 @@ export class BrowserRunner {
}
});
});
this._listeners = [
this.#listeners = [
helper.addEventListener(process, 'exit', this.kill.bind(this)),
];
if (handleSIGINT)
this._listeners.push(
this.#listeners.push(
helper.addEventListener(process, 'SIGINT', () => {
this.kill();
process.exit(130);
})
);
if (handleSIGTERM)
this._listeners.push(
this.#listeners.push(
helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
);
if (handleSIGHUP)
this._listeners.push(
this.#listeners.push(
helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
);
}
close(): Promise<void> {
if (this._closed) return Promise.resolve();
if (this._isTempUserDataDir) {
if (this.#closed) return Promise.resolve();
if (this.#isTempUserDataDir) {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
@ -181,8 +180,8 @@ export class BrowserRunner {
}
// Cleanup this listener last, as that makes sure the full callback runs. If we
// perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners);
return this._processClosing;
helper.removeEventListeners(this.#listeners);
return this.#processClosing;
}
kill(): void {
@ -226,14 +225,14 @@ export class BrowserRunner {
// Attempt to remove temporary profile directory to avoid littering.
try {
if (this._isTempUserDataDir) {
removeFolder.sync(this._userDataDir);
if (this.#isTempUserDataDir) {
removeFolder.sync(this.#userDataDir);
}
} catch (error) {}
// Cleanup this listener last, as that makes sure the full callback runs. If we
// perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners);
helper.removeEventListeners(this.#listeners);
}
async setupConnection(options: {

View File

@ -52,8 +52,17 @@ export interface ProductLauncher {
* @internal
*/
class ChromeLauncher implements ProductLauncher {
/**
* @internal
*/
_projectRoot: string | undefined;
/**
* @internal
*/
_preferredRevision: string;
/**
* @internal
*/
_isPuppeteerCore: boolean;
constructor(
@ -175,7 +184,7 @@ class ChromeLauncher implements ProductLauncher {
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await Browser.create(
browser = await Browser._create(
connection,
[],
ignoreHTTPSErrors,
@ -273,8 +282,17 @@ class ChromeLauncher implements ProductLauncher {
* @internal
*/
class FirefoxLauncher implements ProductLauncher {
/**
* @internal
*/
_projectRoot: string | undefined;
/**
* @internal
*/
_preferredRevision: string;
/**
* @internal
*/
_isPuppeteerCore: boolean;
constructor(
@ -393,7 +411,7 @@ class FirefoxLauncher implements ProductLauncher {
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await Browser.create(
browser = await Browser._create(
connection,
[],
ignoreHTTPSErrors,

View File

@ -50,27 +50,27 @@ export class NodeWebSocketTransport implements ConnectionTransport {
});
}
private _ws: NodeWebSocket;
#ws: NodeWebSocket;
onmessage?: (message: NodeWebSocket.Data) => void;
onclose?: () => void;
constructor(ws: NodeWebSocket) {
this._ws = ws;
this._ws.addEventListener('message', (event) => {
this.#ws = ws;
this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', () => {
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', () => {});
this.#ws.addEventListener('error', () => {});
}
send(message: string): void {
this._ws.send(message);
this.#ws.send(message);
}
close(): void {
this._ws.close();
this.#ws.close();
}
}

View File

@ -22,11 +22,11 @@ import { ConnectionTransport } from '../common/ConnectionTransport.js';
import { assert } from '../common/assert.js';
export class PipeTransport implements ConnectionTransport {
_pipeWrite: NodeJS.WritableStream;
_eventListeners: PuppeteerEventListener[];
#pipeWrite: NodeJS.WritableStream;
#eventListeners: PuppeteerEventListener[];
_isClosed = false;
_pendingMessage = '';
#isClosed = false;
#pendingMessage = '';
onclose?: () => void;
onmessage?: (value: string) => void;
@ -35,10 +35,10 @@ export class PipeTransport implements ConnectionTransport {
pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream
) {
this._pipeWrite = pipeWrite;
this._eventListeners = [
this.#pipeWrite = pipeWrite;
this.#eventListeners = [
helper.addEventListener(pipeRead, 'data', (buffer) =>
this._dispatch(buffer)
this.#dispatch(buffer)
),
helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose) {
@ -51,21 +51,21 @@ export class PipeTransport implements ConnectionTransport {
}
send(message: string): void {
assert(!this._isClosed, '`PipeTransport` is closed.');
assert(!this.#isClosed, '`PipeTransport` is closed.');
this._pipeWrite.write(message);
this._pipeWrite.write('\0');
this.#pipeWrite.write(message);
this.#pipeWrite.write('\0');
}
_dispatch(buffer: Buffer): void {
assert(!this._isClosed, '`PipeTransport` is closed.');
#dispatch(buffer: Buffer): void {
assert(!this.#isClosed, '`PipeTransport` is closed.');
let end = buffer.indexOf('\0');
if (end === -1) {
this._pendingMessage += buffer.toString();
this.#pendingMessage += buffer.toString();
return;
}
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
const message = this.#pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage) {
this.onmessage.call(null, message);
}
@ -79,11 +79,11 @@ export class PipeTransport implements ConnectionTransport {
start = end + 1;
end = buffer.indexOf('\0', start);
}
this._pendingMessage = buffer.toString(undefined, start);
this.#pendingMessage = buffer.toString(undefined, start);
}
close(): void {
this._isClosed = true;
helper.removeEventListeners(this._eventListeners);
this.#isClosed = true;
helper.removeEventListeners(this.#eventListeners);
}
}

View File

@ -77,12 +77,10 @@ export interface PuppeteerLaunchOptions
* @public
*/
export class PuppeteerNode extends Puppeteer {
private _lazyLauncher?: ProductLauncher;
private _projectRoot?: string;
private __productName?: Product;
/**
* @internal
*/
#lazyLauncher?: ProductLauncher;
#projectRoot?: string;
#productName?: Product;
_preferredRevision: string;
/**
@ -98,8 +96,8 @@ export class PuppeteerNode extends Puppeteer {
const { projectRoot, preferredRevision, productName, ...commonSettings } =
settings;
super(commonSettings);
this._projectRoot = projectRoot;
this.__productName = productName;
this.#projectRoot = projectRoot;
this.#productName = productName;
this._preferredRevision = preferredRevision;
this.connect = this.connect.bind(this);
@ -126,13 +124,15 @@ export class PuppeteerNode extends Puppeteer {
* @internal
*/
get _productName(): Product | undefined {
return this.__productName;
return this.#productName;
}
// don't need any TSDoc here - because the getter is internal the setter is too.
/**
* @internal
*/
set _productName(name: Product | undefined) {
if (this.__productName !== name) this._changedProduct = true;
this.__productName = name;
if (this.#productName !== name) this._changedProduct = true;
this.#productName = name;
}
/**
@ -184,8 +184,8 @@ export class PuppeteerNode extends Puppeteer {
*/
get _launcher(): ProductLauncher {
if (
!this._lazyLauncher ||
this._lazyLauncher.product !== this._productName ||
!this.#lazyLauncher ||
this.#lazyLauncher.product !== this._productName ||
this._changedProduct
) {
switch (this._productName) {
@ -197,14 +197,14 @@ export class PuppeteerNode extends Puppeteer {
this._preferredRevision = PUPPETEER_REVISIONS.chromium;
}
this._changedProduct = false;
this._lazyLauncher = Launcher(
this._projectRoot,
this.#lazyLauncher = Launcher(
this.#projectRoot,
this._preferredRevision,
this._isPuppeteerCore,
this._productName
);
}
return this._lazyLauncher;
return this.#lazyLauncher;
}
/**
@ -234,11 +234,11 @@ export class PuppeteerNode extends Puppeteer {
* @returns A new BrowserFetcher instance.
*/
createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher {
if (!this._projectRoot) {
if (!this.#projectRoot) {
throw new Error(
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
return new BrowserFetcher(this._projectRoot, options);
return new BrowserFetcher(this.#projectRoot, options);
}
}

View File

@ -26,7 +26,7 @@ describe('Fixtures', function () {
const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = '';
const { spawn } = require('child_process');
const { spawn } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, {
pipe: true,
dumpio: true,
@ -44,7 +44,7 @@ describe('Fixtures', function () {
const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = '';
const { spawn } = require('child_process');
const { spawn } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, { dumpio: true });
const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'),
@ -58,7 +58,7 @@ describe('Fixtures', function () {
it('should close the browser when the node process closes', async () => {
const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState();
const { spawn, execSync } = require('child_process');
const { spawn, execSync } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, {
// Disable DUMPIO to cleanly read stdout.
dumpio: false,

View File

@ -292,7 +292,7 @@ describe('Frame specs', function () {
describe('Frame.client', function () {
it('should return the client instance', async () => {
const { page } = getTestState();
expect(page.mainFrame().client()).toBeInstanceOf(CDPSession);
expect(page.mainFrame()._client()).toBeInstanceOf(CDPSession);
});
});
});

View File

@ -134,7 +134,7 @@ describeChromeOnly('headful tests', function () {
const browser = await puppeteer.connect({
browserWSEndpoint,
isPageTarget: (target) => {
_isPageTarget(target) {
return (
target.type === 'other' && target.url.startsWith('devtools://')
);

View File

@ -52,18 +52,17 @@ describe('JSHandle', function () {
const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle);
expect(isFive).toBeTruthy();
});
it('should warn on nested object handles', async () => {
it('should warn about recursive objects', async () => {
const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => document.body);
const test: { obj?: unknown } = {};
test.obj = test;
let error = null;
await page
// @ts-expect-error we are deliberately passing a bad type here (nested object)
.evaluateHandle((opts) => opts.elem.querySelector('p'), {
elem: aHandle,
})
.evaluateHandle((opts) => opts.elem, { test })
.catch((error_) => (error = error_));
expect(error.message).toContain('Are you passing a nested JSHandle?');
expect(error.message).toContain('Recursive objects are not allowed.');
});
it('should accept object handle to unserializable value', async () => {
const { page } = getTestState();

View File

@ -21,12 +21,17 @@ import {
describeChromeOnly,
itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions
import {
Browser,
BrowserContext,
Page,
} from '../lib/cjs/puppeteer/api-docs-entry.js';
describeChromeOnly('OOPIF', function () {
/* We use a special browser for this test as we need the --site-per-process flag */
let browser;
let context;
let page;
let browser: Browser;
let context: BrowserContext;
let page: Page;
before(async () => {
const { puppeteer, defaultBrowserOptions } = getTestState();
@ -433,8 +438,8 @@ describeChromeOnly('OOPIF', function () {
});
});
function oopifs(context) {
function oopifs(context: BrowserContext) {
return context
.targets()
.filter((target) => target._targetInfo.type === 'iframe');
.filter((target) => target._getTargetInfo().type === 'iframe');
}

View File

@ -1971,7 +1971,7 @@ describe('Page', function () {
describe('Page.client', function () {
it('should return the client instance', async () => {
const { page } = getTestState();
expect(page.client()).toBeInstanceOf(CDPSession);
expect(page._client()).toBeInstanceOf(CDPSession);
});
});
});