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 = { module.exports = {
root: true, root: true,
env: { env: {
@ -15,14 +24,6 @@ module.exports = {
// Error if files are not formatted with Prettier correctly. // Error if files are not formatted with Prettier correctly.
'prettier/prettier': 2, 'prettier/prettier': 2,
// syntax preferences // syntax preferences
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'spaced-comment': [ 'spaced-comment': [
2, 2,
'always', 'always',
@ -116,6 +117,12 @@ module.exports = {
}, },
], ],
'import/extensions': ['error', 'ignorePackages'], 'import/extensions': ['error', 'ignorePackages'],
'no-restricted-syntax': [
'error',
// Don't allow underscored declarations on camelCased variables/properties.
// ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
],
}, },
overrides: [ overrides: [
{ {
@ -144,8 +151,6 @@ module.exports = {
// We don't require explicit return types on basic functions or // We don't require explicit return types on basic functions or
// dummy functions in tests, for example // dummy functions in tests, for example
'@typescript-eslint/explicit-function-return-type': 0, '@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. // We allow non-null assertions if the value was asserted using `assert` API.
'@typescript-eslint/no-non-null-assertion': 0, '@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. // By default this is a warning but we want it to error.
'@typescript-eslint/explicit-module-boundary-types': 2, '@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 * @public
*/ */
export class Accessibility { export class Accessibility {
private _client: CDPSession; #client: CDPSession;
/** /**
* @internal * @internal
*/ */
constructor(client: CDPSession) { constructor(client: CDPSession) {
this._client = client; this.#client = client;
} }
/** /**
@ -182,10 +182,10 @@ export class Accessibility {
options: SnapshotOptions = {} options: SnapshotOptions = {}
): Promise<SerializedAXNode | null> { ): Promise<SerializedAXNode | null> {
const { interestingOnly = true, root = null } = options; 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; let backendNodeId: number | undefined;
if (root) { if (root) {
const { node } = await this._client.send('DOM.describeNode', { const { node } = await this.#client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId, objectId: root._remoteObject.objectId,
}); });
backendNodeId = node.backendNodeId; backendNodeId = node.backendNodeId;
@ -238,53 +238,53 @@ class AXNode {
public payload: Protocol.Accessibility.AXNode; public payload: Protocol.Accessibility.AXNode;
public children: AXNode[] = []; public children: AXNode[] = [];
private _richlyEditable = false; #richlyEditable = false;
private _editable = false; #editable = false;
private _focusable = false; #focusable = false;
private _hidden = false; #hidden = false;
private _name: string; #name: string;
private _role: string; #role: string;
private _ignored: boolean; #ignored: boolean;
private _cachedHasFocusableChild?: boolean; #cachedHasFocusableChild?: boolean;
constructor(payload: Protocol.Accessibility.AXNode) { constructor(payload: Protocol.Accessibility.AXNode) {
this.payload = payload; this.payload = payload;
this._name = this.payload.name ? this.payload.name.value : ''; this.#name = this.payload.name ? this.payload.name.value : '';
this._role = this.payload.role ? this.payload.role.value : 'Unknown'; this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
this._ignored = this.payload.ignored; this.#ignored = this.payload.ignored;
for (const property of this.payload.properties || []) { for (const property of this.payload.properties || []) {
if (property.name === 'editable') { if (property.name === 'editable') {
this._richlyEditable = property.value.value === 'richtext'; this.#richlyEditable = property.value.value === 'richtext';
this._editable = true; this.#editable = true;
} }
if (property.name === 'focusable') this._focusable = property.value.value; if (property.name === 'focusable') this.#focusable = property.value.value;
if (property.name === 'hidden') this._hidden = property.value.value; if (property.name === 'hidden') this.#hidden = property.value.value;
} }
} }
private _isPlainTextField(): boolean { #isPlainTextField(): boolean {
if (this._richlyEditable) return false; if (this.#richlyEditable) return false;
if (this._editable) return true; if (this.#editable) return true;
return this._role === 'textbox' || this._role === 'searchbox'; return this.#role === 'textbox' || this.#role === 'searchbox';
} }
private _isTextOnlyObject(): boolean { #isTextOnlyObject(): boolean {
const role = this._role; const role = this.#role;
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox'; return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
} }
private _hasFocusableChild(): boolean { #hasFocusableChild(): boolean {
if (this._cachedHasFocusableChild === undefined) { if (this.#cachedHasFocusableChild === undefined) {
this._cachedHasFocusableChild = false; this.#cachedHasFocusableChild = false;
for (const child of this.children) { for (const child of this.children) {
if (child._focusable || child._hasFocusableChild()) { if (child.#focusable || child.#hasFocusableChild()) {
this._cachedHasFocusableChild = true; this.#cachedHasFocusableChild = true;
break; break;
} }
} }
} }
return this._cachedHasFocusableChild; return this.#cachedHasFocusableChild;
} }
public find(predicate: (x: AXNode) => boolean): AXNode | null { 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 // implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find // accessibility APIs because screen readers might be confused if they find
// any children. // 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 // Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers. // HTML5 Specs should be hidden from screen readers.
// (Note that whilst ARIA buttons can have only presentational children, HTML5 // (Note that whilst ARIA buttons can have only presentational children, HTML5
// buttons are allowed to have content.) // buttons are allowed to have content.)
switch (this._role) { switch (this.#role) {
case 'doc-cover': case 'doc-cover':
case 'graphics-symbol': case 'graphics-symbol':
case 'img': case 'img':
@ -324,14 +324,14 @@ class AXNode {
} }
// Here and below: Android heuristics // Here and below: Android heuristics
if (this._hasFocusableChild()) return false; if (this.#hasFocusableChild()) return false;
if (this._focusable && this._name) return true; if (this.#focusable && this.#name) return true;
if (this._role === 'heading' && this._name) return true; if (this.#role === 'heading' && this.#name) return true;
return false; return false;
} }
public isControl(): boolean { public isControl(): boolean {
switch (this._role) { switch (this.#role) {
case 'button': case 'button':
case 'checkbox': case 'checkbox':
case 'ColorWell': case 'ColorWell':
@ -360,10 +360,10 @@ class AXNode {
} }
public isInteresting(insideControl: boolean): boolean { public isInteresting(insideControl: boolean): boolean {
const role = this._role; const role = this.#role;
if (role === 'Ignored' || this._hidden || this._ignored) return false; 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 it's not focusable but has a control role, then it's interesting.
if (this.isControl()) return true; if (this.isControl()) return true;
@ -371,7 +371,7 @@ class AXNode {
// A non focusable child of a control is not interesting // A non focusable child of a control is not interesting
if (insideControl) return false; if (insideControl) return false;
return this.isLeafNode() && !!this._name; return this.isLeafNode() && !!this.#name;
} }
public serialize(): SerializedAXNode { public serialize(): SerializedAXNode {
@ -384,7 +384,7 @@ class AXNode {
properties.set('description', this.payload.description.value); properties.set('description', this.payload.description.value);
const node: SerializedAXNode = { const node: SerializedAXNode = {
role: this._role, role: this.#role,
}; };
type UserStringProperty = type UserStringProperty =
@ -440,7 +440,7 @@ class AXNode {
// RootWebArea's treat focus differently than other nodes. They report whether // RootWebArea's treat focus differently than other nodes. They report whether
// their frame has focus, not whether focus is specifically on the root // their frame has focus, not whether focus is specifically on the root
// node. // node.
if (booleanProperty === 'focused' && this._role === 'RootWebArea') if (booleanProperty === 'focused' && this.#role === 'RootWebArea')
continue; continue;
const value = getBooleanPropertyValue(booleanProperty); const value = getBooleanPropertyValue(booleanProperty);
if (!value) continue; if (!value) continue;

View File

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

View File

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

View File

@ -54,7 +54,7 @@ export interface BrowserConnectOptions {
/** /**
* @internal * @internal
*/ */
isPageTarget?: IsPageTargetCallback; _isPageTarget?: IsPageTargetCallback;
} }
const getWebSocketTransportClass = async () => { const getWebSocketTransportClass = async () => {
@ -67,15 +67,16 @@ const getWebSocketTransportClass = async () => {
/** /**
* Users should never call this directly; it's called when calling * Users should never call this directly; it's called when calling
* `puppeteer.connect`. * `puppeteer.connect`.
*
* @internal * @internal
*/ */
export const connectToBrowser = async ( export async function _connectToBrowser(
options: BrowserConnectOptions & { options: BrowserConnectOptions & {
browserWSEndpoint?: string; browserWSEndpoint?: string;
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
} }
): Promise<Browser> => { ): Promise<Browser> {
const { const {
browserWSEndpoint, browserWSEndpoint,
browserURL, browserURL,
@ -84,7 +85,7 @@ export const connectToBrowser = async (
transport, transport,
slowMo = 0, slowMo = 0,
targetFilter, targetFilter,
isPageTarget, _isPageTarget: isPageTarget,
} = options; } = options;
assert( assert(
@ -112,7 +113,7 @@ export const connectToBrowser = async (
const { browserContextIds } = await connection.send( const { browserContextIds } = await connection.send(
'Target.getBrowserContexts' 'Target.getBrowserContexts'
); );
return Browser.create( return Browser._create(
connection, connection,
browserContextIds, browserContextIds,
ignoreHTTPSErrors, ignoreHTTPSErrors,
@ -122,7 +123,7 @@ export const connectToBrowser = async (
targetFilter, targetFilter,
isPageTarget isPageTarget
); );
}; }
async function getWSEndpoint(browserURL: string): Promise<string> { async function getWSEndpoint(browserURL: string): Promise<string> {
const endpointURL = new URL('/json/version', browserURL); 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; onmessage?: (message: string) => void;
onclose?: () => void; onclose?: () => void;
constructor(ws: WebSocket) { constructor(ws: WebSocket) {
this._ws = ws; this.#ws = ws;
this._ws.addEventListener('message', (event) => { this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data); if (this.onmessage) this.onmessage.call(null, event.data);
}); });
this._ws.addEventListener('close', () => { this.#ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null); if (this.onclose) this.onclose.call(null);
}); });
// Silently ignore all errors - we don't know what to do with them. // 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 { send(message: string): void {
this._ws.send(message); this.#ws.send(message);
} }
close(): void { close(): void {
this._ws.close(); this.#ws.close();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@
import { assert } from './assert.js'; import { assert } from './assert.js';
import { helper } from './helper.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 { CDPSession } from './Connection.js';
import { DOMWorld } from './DOMWorld.js'; import { DOMWorld } from './DOMWorld.js';
import { Frame } from './FrameManager.js'; import { Frame } from './FrameManager.js';
@ -137,11 +137,7 @@ export class ExecutionContext {
pageFunction: Function | string, pageFunction: Function | string,
...args: unknown[] ...args: unknown[]
): Promise<ReturnType> { ): Promise<ReturnType> {
return await this._evaluateInternal<ReturnType>( return await this.#evaluate<ReturnType>(true, pageFunction, ...args);
true,
pageFunction,
...args
);
} }
/** /**
@ -190,10 +186,10 @@ export class ExecutionContext {
pageFunction: EvaluateHandleFn, pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<HandleType> { ): 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, returnByValue: boolean,
pageFunction: Function | string, pageFunction: Function | string,
...args: unknown[] ...args: unknown[]
@ -224,7 +220,7 @@ export class ExecutionContext {
return returnByValue return returnByValue
? helper.valueFromRemoteObject(remoteObject) ? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : _createJSHandle(this, remoteObject);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function')
@ -263,8 +259,9 @@ export class ExecutionContext {
if ( if (
error instanceof TypeError && error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON') 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; throw error;
} }
const { exceptionDetails, result: remoteObject } = const { exceptionDetails, result: remoteObject } =
@ -275,7 +272,7 @@ export class ExecutionContext {
); );
return returnByValue return returnByValue
? helper.valueFromRemoteObject(remoteObject) ? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : _createJSHandle(this, remoteObject);
function convertArgument( function convertArgument(
this: ExecutionContext, this: ExecutionContext,
@ -355,7 +352,7 @@ export class ExecutionContext {
const response = await this._client.send('Runtime.queryObjects', { const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId, prototypeObjectId: prototypeHandle._remoteObject.objectId,
}); });
return createJSHandle(this, response.objects); return _createJSHandle(this, response.objects);
} }
/** /**
@ -368,7 +365,7 @@ export class ExecutionContext {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,
executionContextId: this._contextId, 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 * @public
*/ */
export class FileChooser { export class FileChooser {
private _element: ElementHandle; #element: ElementHandle;
private _multiple: boolean; #multiple: boolean;
private _handled = false; #handled = false;
/** /**
* @internal * @internal
@ -48,15 +48,15 @@ export class FileChooser {
element: ElementHandle, element: ElementHandle,
event: Protocol.Page.FileChooserOpenedEvent event: Protocol.Page.FileChooserOpenedEvent
) { ) {
this._element = element; this.#element = element;
this._multiple = event.mode !== 'selectSingle'; 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. * 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 { isMultiple(): boolean {
return this._multiple; return this.#multiple;
} }
/** /**
@ -66,11 +66,11 @@ export class FileChooser {
*/ */
async accept(filePaths: string[]): Promise<void> { async accept(filePaths: string[]): Promise<void> {
assert( assert(
!this._handled, !this.#handled,
'Cannot accept FileChooser which is already handled!' 'Cannot accept FileChooser which is already handled!'
); );
this._handled = true; this.#handled = true;
await this._element.uploadFile(...filePaths); await this.#element.uploadFile(...filePaths);
} }
/** /**
@ -78,9 +78,9 @@ export class FileChooser {
*/ */
cancel(): void { cancel(): void {
assert( assert(
!this._handled, !this.#handled,
'Cannot cancel FileChooser which is already handled!' 'Cannot cancel FileChooser which is already handled!'
); );
this._handled = true; this.#handled = true;
} }
} }

View File

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

View File

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

View File

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

View File

@ -16,7 +16,11 @@
import { assert } from './assert.js'; import { assert } from './assert.js';
import { CDPSession } from './Connection.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 { Protocol } from 'devtools-protocol';
import { Point } from './JSHandle.js'; import { Point } from './JSHandle.js';
@ -64,14 +68,19 @@ type KeyDescription = Required<
* @public * @public
*/ */
export class Keyboard { export class Keyboard {
private _client: CDPSession; #client: CDPSession;
/** @internal */ #pressedKeys = new Set<string>();
_modifiers = 0;
private _pressedKeys = new Set<string>();
/** @internal */ /**
* @internal
*/
_modifiers = 0;
/**
* @internal
*/
constructor(client: CDPSession) { constructor(client: CDPSession) {
this._client = client; this.#client = client;
} }
/** /**
@ -103,14 +112,14 @@ export class Keyboard {
key: KeyInput, key: KeyInput,
options: { text?: string } = { text: undefined } options: { text?: string } = { text: undefined }
): Promise<void> { ): Promise<void> {
const description = this._keyDescriptionForString(key); const description = this.#keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code); const autoRepeat = this.#pressedKeys.has(description.code);
this._pressedKeys.add(description.code); this.#pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key); this._modifiers |= this.#modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text; 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', type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers, modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode, 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 === 'Alt') return 1;
if (key === 'Control') return 2; if (key === 'Control') return 2;
if (key === 'Meta') return 4; if (key === 'Meta') return 4;
@ -132,7 +141,7 @@ export class Keyboard {
return 0; return 0;
} }
private _keyDescriptionForString(keyString: KeyInput): KeyDescription { #keyDescriptionForString(keyString: KeyInput): KeyDescription {
const shift = this._modifiers & 8; const shift = this._modifiers & 8;
const description = { const description = {
key: '', key: '',
@ -142,7 +151,7 @@ export class Keyboard {
location: 0, location: 0,
}; };
const definition = keyDefinitions[keyString]; const definition = _keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`); assert(definition, `Unknown key: "${keyString}"`);
if (definition.key) description.key = definition.key; if (definition.key) description.key = definition.key;
@ -175,11 +184,11 @@ export class Keyboard {
* for a list of all key names. * for a list of all key names.
*/ */
async up(key: KeyInput): Promise<void> { async up(key: KeyInput): Promise<void> {
const description = this._keyDescriptionForString(key); const description = this.#keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key); this._modifiers &= ~this.#modifierBit(description.key);
this._pressedKeys.delete(description.code); this.#pressedKeys.delete(description.code);
await this._client.send('Input.dispatchKeyEvent', { await this.#client.send('Input.dispatchKeyEvent', {
type: 'keyUp', type: 'keyUp',
modifiers: this._modifiers, modifiers: this._modifiers,
key: description.key, key: description.key,
@ -205,11 +214,11 @@ export class Keyboard {
* @param char - Character to send into the page. * @param char - Character to send into the page.
*/ */
async sendCharacter(char: string): Promise<void> { 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 { 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 * @public
*/ */
export class Mouse { export class Mouse {
private _client: CDPSession; #client: CDPSession;
private _keyboard: Keyboard; #keyboard: Keyboard;
private _x = 0; #x = 0;
private _y = 0; #y = 0;
private _button: MouseButton | 'none' = 'none'; #button: MouseButton | 'none' = 'none';
/** /**
* @internal * @internal
*/ */
constructor(client: CDPSession, keyboard: Keyboard) { constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client; this.#client = client;
this._keyboard = keyboard; this.#keyboard = keyboard;
} }
/** /**
@ -383,17 +392,17 @@ export class Mouse {
options: { steps?: number } = {} options: { steps?: number } = {}
): Promise<void> { ): Promise<void> {
const { steps = 1 } = options; const { steps = 1 } = options;
const fromX = this._x, const fromX = this.#x,
fromY = this._y; fromY = this.#y;
this._x = x; this.#x = x;
this._y = y; this.#y = y;
for (let i = 1; i <= steps; i++) { for (let i = 1; i <= steps; i++) {
await this._client.send('Input.dispatchMouseEvent', { await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved', type: 'mouseMoved',
button: this._button, button: this.#button,
x: fromX + (this._x - fromX) * (i / steps), x: fromX + (this.#x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps), y: fromY + (this.#y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
}); });
} }
} }
@ -428,13 +437,13 @@ export class Mouse {
*/ */
async down(options: MouseOptions = {}): Promise<void> { async down(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options; const { button = 'left', clickCount = 1 } = options;
this._button = button; this.#button = button;
await this._client.send('Input.dispatchMouseEvent', { await this.#client.send('Input.dispatchMouseEvent', {
type: 'mousePressed', type: 'mousePressed',
button, button,
x: this._x, x: this.#x,
y: this._y, y: this.#y,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
clickCount, clickCount,
}); });
} }
@ -445,13 +454,13 @@ export class Mouse {
*/ */
async up(options: MouseOptions = {}): Promise<void> { async up(options: MouseOptions = {}): Promise<void> {
const { button = 'left', clickCount = 1 } = options; const { button = 'left', clickCount = 1 } = options;
this._button = 'none'; this.#button = 'none';
await this._client.send('Input.dispatchMouseEvent', { await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased', type: 'mouseReleased',
button, button,
x: this._x, x: this.#x,
y: this._y, y: this.#y,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
clickCount, clickCount,
}); });
} }
@ -477,13 +486,13 @@ export class Mouse {
*/ */
async wheel(options: MouseWheelOptions = {}): Promise<void> { async wheel(options: MouseWheelOptions = {}): Promise<void> {
const { deltaX = 0, deltaY = 0 } = options; const { deltaX = 0, deltaY = 0 } = options;
await this._client.send('Input.dispatchMouseEvent', { await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel', type: 'mouseWheel',
x: this._x, x: this.#x,
y: this._y, y: this.#y,
deltaX, deltaX,
deltaY, deltaY,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
pointerType: 'mouse', pointerType: 'mouse',
}); });
} }
@ -495,7 +504,7 @@ export class Mouse {
*/ */
async drag(start: Point, target: Point): Promise<Protocol.Input.DragData> { async drag(start: Point, target: Point): Promise<Protocol.Input.DragData> {
const promise = new Promise<Protocol.Input.DragData>((resolve) => { const promise = new Promise<Protocol.Input.DragData>((resolve) => {
this._client.once('Input.dragIntercepted', (event) => this.#client.once('Input.dragIntercepted', (event) =>
resolve(event.data) resolve(event.data)
); );
}); });
@ -511,11 +520,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask * @param data - drag data containing items and operations mask
*/ */
async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void> { async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', { await this.#client.send('Input.dispatchDragEvent', {
type: 'dragEnter', type: 'dragEnter',
x: target.x, x: target.x,
y: target.y, y: target.y,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
data, data,
}); });
} }
@ -526,11 +535,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask * @param data - drag data containing items and operations mask
*/ */
async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void> { async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', { await this.#client.send('Input.dispatchDragEvent', {
type: 'dragOver', type: 'dragOver',
x: target.x, x: target.x,
y: target.y, y: target.y,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
data, data,
}); });
} }
@ -541,11 +550,11 @@ export class Mouse {
* @param data - drag data containing items and operations mask * @param data - drag data containing items and operations mask
*/ */
async drop(target: Point, data: Protocol.Input.DragData): Promise<void> { async drop(target: Point, data: Protocol.Input.DragData): Promise<void> {
await this._client.send('Input.dispatchDragEvent', { await this.#client.send('Input.dispatchDragEvent', {
type: 'drop', type: 'drop',
x: target.x, x: target.x,
y: target.y, y: target.y,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
data, data,
}); });
} }
@ -580,15 +589,15 @@ export class Mouse {
* @public * @public
*/ */
export class Touchscreen { export class Touchscreen {
private _client: CDPSession; #client: CDPSession;
private _keyboard: Keyboard; #keyboard: Keyboard;
/** /**
* @internal * @internal
*/ */
constructor(client: CDPSession, keyboard: Keyboard) { constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client; this.#client = client;
this._keyboard = keyboard; this.#keyboard = keyboard;
} }
/** /**
@ -598,15 +607,15 @@ export class Touchscreen {
*/ */
async tap(x: number, y: number): Promise<void> { async tap(x: number, y: number): Promise<void> {
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }]; const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this._client.send('Input.dispatchTouchEvent', { await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchStart', type: 'touchStart',
touchPoints, touchPoints,
modifiers: this._keyboard._modifiers, modifiers: this.#keyboard._modifiers,
}); });
await this._client.send('Input.dispatchTouchEvent', { await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchEnd', type: 'touchEnd',
touchPoints: [], 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 { debugError, helper } from './helper.js';
import { MouseButton } from './Input.js'; import { MouseButton } from './Input.js';
import { Page, ScreenshotOptions } from './Page.js'; import { Page, ScreenshotOptions } from './Page.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js'; import { _getQueryHandlerAndSelector } from './QueryHandler.js';
import { KeyInput } from './USKeyboardLayout.js'; import { KeyInput } from './USKeyboardLayout.js';
/** /**
@ -62,7 +62,7 @@ export interface BoundingBox extends Point {
/** /**
* @internal * @internal
*/ */
export function createJSHandle( export function _createJSHandle(
context: ExecutionContext, context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
): JSHandle { ): JSHandle {
@ -103,22 +103,38 @@ const applyOffsetsToQuad = (quad: Point[], offsetX: number, offsetY: number) =>
* @public * @public
*/ */
export class JSHandle<HandleObjectType = unknown> { export class JSHandle<HandleObjectType = unknown> {
#client: CDPSession;
#disposed = false;
#context: ExecutionContext;
#remoteObject: Protocol.Runtime.RemoteObject;
/** /**
* @internal * @internal
*/ */
_context: ExecutionContext; get _client(): CDPSession {
return this.#client;
}
/** /**
* @internal * @internal
*/ */
_client: CDPSession; get _disposed(): boolean {
return this.#disposed;
}
/** /**
* @internal * @internal
*/ */
_remoteObject: Protocol.Runtime.RemoteObject; get _remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
/** /**
* @internal * @internal
*/ */
_disposed = false; get _context(): ExecutionContext {
return this.#context;
}
/** /**
* @internal * @internal
@ -128,15 +144,15 @@ export class JSHandle<HandleObjectType = unknown> {
client: CDPSession, client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
) { ) {
this._context = context; this.#context = context;
this._client = client; this.#client = client;
this._remoteObject = remoteObject; this.#remoteObject = remoteObject;
} }
/** Returns the execution context the handle belongs to. /** Returns the execution context the handle belongs to.
*/ */
executionContext(): ExecutionContext { executionContext(): ExecutionContext {
return this._context; return this.#context;
} }
/** /**
@ -222,15 +238,15 @@ export class JSHandle<HandleObjectType = unknown> {
* ``` * ```
*/ */
async getProperties(): Promise<Map<string, JSHandle>> { async getProperties(): Promise<Map<string, JSHandle>> {
assert(this._remoteObject.objectId); assert(this.#remoteObject.objectId);
const response = await this._client.send('Runtime.getProperties', { const response = await this.#client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId, objectId: this.#remoteObject.objectId,
ownProperties: true, ownProperties: true,
}); });
const result = new Map<string, JSHandle>(); const result = new Map<string, JSHandle>();
for (const property of response.result) { for (const property of response.result) {
if (!property.enumerable || !property.value) continue; 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; return result;
} }
@ -245,16 +261,16 @@ export class JSHandle<HandleObjectType = unknown> {
* **NOTE** The method throws if the referenced object is not stringifiable. * **NOTE** The method throws if the referenced object is not stringifiable.
*/ */
async jsonValue<T = unknown>(): Promise<T> { async jsonValue<T = unknown>(): Promise<T> {
if (this._remoteObject.objectId) { if (this.#remoteObject.objectId) {
const response = await this._client.send('Runtime.callFunctionOn', { const response = await this.#client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }', functionDeclaration: 'function() { return this; }',
objectId: this._remoteObject.objectId, objectId: this.#remoteObject.objectId,
returnByValue: true, returnByValue: true,
awaitPromise: true, awaitPromise: true,
}); });
return helper.valueFromRemoteObject(response.result) as T; 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. * successfully disposed of.
*/ */
async dispose(): Promise<void> { async dispose(): Promise<void> {
if (this._disposed) return; if (this.#disposed) return;
this._disposed = true; this.#disposed = true;
await helper.releaseObject(this._client, this._remoteObject); await helper.releaseObject(this.#client, this.#remoteObject);
} }
/** /**
@ -284,11 +300,11 @@ export class JSHandle<HandleObjectType = unknown> {
* @remarks Useful during debugging. * @remarks Useful during debugging.
*/ */
toString(): string { toString(): string {
if (this._remoteObject.objectId) { if (this.#remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type; const type = this.#remoteObject.subtype || this.#remoteObject.type;
return 'JSHandle@' + 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< export class ElementHandle<
ElementType extends Element = Element ElementType extends Element = Element
> extends JSHandle<ElementType> { > extends JSHandle<ElementType> {
private _frame: Frame; #frame: Frame;
private _page: Page; #page: Page;
private _frameManager: FrameManager; #frameManager: FrameManager;
/** /**
* @internal * @internal
@ -345,11 +361,9 @@ export class ElementHandle<
frameManager: FrameManager frameManager: FrameManager
) { ) {
super(context, client, remoteObject); super(context, client, remoteObject);
this._client = client; this.#frame = frame;
this._remoteObject = remoteObject; this.#page = page;
this._frame = frame; this.#frameManager = frameManager;
this._page = page;
this._frameManager = frameManager;
} }
/** /**
@ -498,10 +512,10 @@ export class ElementHandle<
objectId: this._remoteObject.objectId, objectId: this._remoteObject.objectId,
}); });
if (typeof nodeInfo.node.frameId !== 'string') return null; 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( const error = await this.evaluate(
async ( async (
element: Element, element: Element,
@ -541,13 +555,13 @@ export class ElementHandle<
} }
return false; return false;
}, },
this._page.isJavaScriptEnabled() this.#page.isJavaScriptEnabled()
); );
if (error) throw new Error(error); if (error) throw new Error(error);
} }
private async _getOOPIFOffsets( async #getOOPIFOffsets(
frame: Frame frame: Frame
): Promise<{ offsetX: number; offsetY: number }> { ): Promise<{ offsetX: number; offsetY: number }> {
let offsetX = 0; let offsetX = 0;
@ -559,17 +573,19 @@ export class ElementHandle<
currentFrame = parent; currentFrame = parent;
continue; continue;
} }
const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', { const { backendNodeId } = await parent
frameId: currentFrame._id, ._client()
}); .send('DOM.getFrameOwner', {
const result = await parent._client.send('DOM.getBoxModel', { frameId: currentFrame._id,
});
const result = await parent._client().send('DOM.getBoxModel', {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,
}); });
if (!result) { if (!result) {
break; break;
} }
const contentBoxQuad = result.model.content; const contentBoxQuad = result.model.content;
const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0]; const topLeftCorner = this.#fromProtocolQuad(contentBoxQuad)[0];
offsetX += topLeftCorner!.x; offsetX += topLeftCorner!.x;
offsetY += topLeftCorner!.y; offsetY += topLeftCorner!.y;
currentFrame = parent; currentFrame = parent;
@ -587,7 +603,7 @@ export class ElementHandle<
objectId: this._remoteObject.objectId, objectId: this._remoteObject.objectId,
}) })
.catch(debugError), .catch(debugError),
this._page.client().send('Page.getLayoutMetrics'), this.#page._client().send('Page.getLayoutMetrics'),
]); ]);
if (!result || !result.quads.length) if (!result || !result.quads.length)
throw new Error('Node is either not clickable or not an HTMLElement'); 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. // Fallback to `layoutViewport` in case of using Firefox.
const { clientWidth, clientHeight } = const { clientWidth, clientHeight } =
layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport; layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport;
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame); const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
const quads = result.quads const quads = result.quads
.map((quad) => this._fromProtocolQuad(quad)) .map((quad) => this.#fromProtocolQuad(quad))
.map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY)) .map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY))
.map((quad) => .map((quad) =>
this._intersectQuadWithViewport(quad, clientWidth, clientHeight) this.#intersectQuadWithViewport(quad, clientWidth, clientHeight)
) )
.filter((quad) => computeQuadArea(quad) > 1); .filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length) 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 = { const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this._remoteObject.objectId, objectId: this._remoteObject.objectId,
}; };
@ -650,7 +666,7 @@ export class ElementHandle<
.catch((error) => debugError(error)); .catch((error) => debugError(error));
} }
private _fromProtocolQuad(quad: number[]): Point[] { #fromProtocolQuad(quad: number[]): Point[] {
return [ return [
{ x: quad[0]!, y: quad[1]! }, { x: quad[0]!, y: quad[1]! },
{ x: quad[2]!, y: quad[3]! }, { x: quad[2]!, y: quad[3]! },
@ -659,7 +675,7 @@ export class ElementHandle<
]; ];
} }
private _intersectQuadWithViewport( #intersectQuadWithViewport(
quad: Point[], quad: Point[],
width: number, width: number,
height: number height: number
@ -676,9 +692,9 @@ export class ElementHandle<
* If the element is detached from DOM, the method throws an error. * If the element is detached from DOM, the method throws an error.
*/ */
async hover(): Promise<void> { async hover(): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint(); 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. * If the element is detached from DOM, the method throws an error.
*/ */
async click(options: ClickOptions = {}): Promise<void> { async click(options: ClickOptions = {}): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint(options.offset); 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> { async drag(target: Point): Promise<Protocol.Input.DragData> {
assert( assert(
this._page.isDragInterceptionEnabled(), this.#page.isDragInterceptionEnabled(),
'Drag Interception is not enabled!' 'Drag Interception is not enabled!'
); );
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const start = await this.clickablePoint(); 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( async dragEnter(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> { ): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const target = await this.clickablePoint(); 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( async dragOver(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> { ): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const target = await this.clickablePoint(); 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( async drop(
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 } data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
): Promise<void> { ): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const destination = await this.clickablePoint(); 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, target: ElementHandle,
options?: { delay: number } options?: { delay: number }
): Promise<void> { ): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const startPoint = await this.clickablePoint(); const startPoint = await this.clickablePoint();
const targetPoint = await target.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. * If the element is detached from DOM, the method throws an error.
*/ */
async tap(): Promise<void> { async tap(): Promise<void> {
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint(); 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> { async type(text: string, options?: { delay: number }): Promise<void> {
await this.focus(); 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> { async press(key: KeyInput, options?: PressOptions): Promise<void> {
await this.focus(); 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. * or `null` if the element is not visible.
*/ */
async boundingBox(): Promise<BoundingBox | null> { async boundingBox(): Promise<BoundingBox | null> {
const result = await this._getBoxModel(); const result = await this.#getBoxModel();
if (!result) return null; 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 quad = result.model.border;
const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!); const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!);
const y = Math.min(quad[1]!, quad[3]!, quad[5]!, quad[7]!); 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. * Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/ */
async boxModel(): Promise<BoxModel | null> { async boxModel(): Promise<BoxModel | null> {
const result = await this._getBoxModel(); const result = await this.#getBoxModel();
if (!result) return null; 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; const { content, padding, border, margin, width, height } = result.model;
return { return {
content: applyOffsetsToQuad( content: applyOffsetsToQuad(
this._fromProtocolQuad(content), this.#fromProtocolQuad(content),
offsetX, offsetX,
offsetY offsetY
), ),
padding: applyOffsetsToQuad( padding: applyOffsetsToQuad(
this._fromProtocolQuad(padding), this.#fromProtocolQuad(padding),
offsetX, offsetX,
offsetY offsetY
), ),
border: applyOffsetsToQuad( border: applyOffsetsToQuad(
this._fromProtocolQuad(border), this.#fromProtocolQuad(border),
offsetX, offsetX,
offsetY offsetY
), ),
margin: applyOffsetsToQuad( margin: applyOffsetsToQuad(
this._fromProtocolQuad(margin), this.#fromProtocolQuad(margin),
offsetX, offsetX,
offsetY offsetY
), ),
@ -1015,7 +1031,7 @@ export class ElementHandle<
let boundingBox = await this.boundingBox(); let boundingBox = await this.boundingBox();
assert(boundingBox, 'Node is either not visible or not an HTMLElement'); assert(boundingBox, 'Node is either not visible or not an HTMLElement');
const viewport = this._page.viewport(); const viewport = this.#page.viewport();
assert(viewport); assert(viewport);
if ( if (
@ -1026,12 +1042,12 @@ export class ElementHandle<
width: Math.max(viewport.width, Math.ceil(boundingBox.width)), width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
height: Math.max(viewport.height, Math.ceil(boundingBox.height)), 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; needsViewportReset = true;
} }
await this._scrollIntoViewIfNeeded(); await this.#scrollIntoViewIfNeeded();
boundingBox = await this.boundingBox(); boundingBox = await this.boundingBox();
assert(boundingBox, 'Node is either not visible or not an HTMLElement'); assert(boundingBox, 'Node is either not visible or not an HTMLElement');
@ -1047,7 +1063,7 @@ export class ElementHandle<
clip.x += pageX; clip.x += pageX;
clip.y += pageY; clip.y += pageY;
const imageData = await this._page.screenshot( const imageData = await this.#page.screenshot(
Object.assign( 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; return imageData;
} }
@ -1073,7 +1089,7 @@ export class ElementHandle<
selector: string selector: string
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<T> | null> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert( assert(
queryHandler.queryOne, queryHandler.queryOne,
'Cannot handle queries for a single element with the given selector' 'Cannot handle queries for a single element with the given selector'
@ -1096,7 +1112,7 @@ export class ElementHandle<
selector: string selector: string
): Promise<Array<ElementHandle<T>>> { ): Promise<Array<ElementHandle<T>>> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert( assert(
queryHandler.queryAll, queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector' 'Cannot handle queries for a multiple element with the given selector'
@ -1184,7 +1200,7 @@ export class ElementHandle<
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> { ): Promise<WrapElementHandle<ReturnType>> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray); assert(queryHandler.queryAllArray);
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>( const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>(

View File

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

View File

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

View File

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

View File

@ -182,17 +182,19 @@ export interface PaperFormatDimensions {
/** /**
* @internal * @internal
*/ */
export const paperFormats: Record<LowerCasePaperFormat, PaperFormatDimensions> = export const _paperFormats: Record<
{ LowerCasePaperFormat,
letter: { width: 8.5, height: 11 }, PaperFormatDimensions
legal: { width: 8.5, height: 14 }, > = {
tabloid: { width: 11, height: 17 }, letter: { width: 8.5, height: 11 },
ledger: { width: 17, height: 11 }, legal: { width: 8.5, height: 14 },
a0: { width: 33.1, height: 46.8 }, tabloid: { width: 11, height: 17 },
a1: { width: 23.4, height: 33.1 }, ledger: { width: 17, height: 11 },
a2: { width: 16.54, height: 23.4 }, a0: { width: 33.1, height: 46.8 },
a3: { width: 11.7, height: 16.54 }, a1: { width: 23.4, height: 33.1 },
a4: { width: 8.27, height: 11.7 }, a2: { width: 16.54, height: 23.4 },
a5: { width: 5.83, height: 8.27 }, a3: { width: 11.7, height: 16.54 },
a6: { width: 4.13, height: 5.83 }, a4: { width: 8.27, height: 11.7 },
} as const; 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 { puppeteerErrors, PuppeteerErrors } from './Errors.js';
import { ConnectionTransport } from './ConnectionTransport.js'; import { ConnectionTransport } from './ConnectionTransport.js';
import { devicesMap, DevicesMap } from './DeviceDescriptors.js'; import { _devicesMap, DevicesMap } from './DeviceDescriptors.js';
import { Browser } from './Browser.js'; import { Browser } from './Browser.js';
import { import {
registerCustomQueryHandler, _registerCustomQueryHandler,
unregisterCustomQueryHandler, _unregisterCustomQueryHandler,
customQueryHandlerNames, _customQueryHandlerNames,
clearCustomQueryHandlers, _clearCustomQueryHandlers,
CustomQueryHandler, CustomQueryHandler,
} from './QueryHandler.js'; } from './QueryHandler.js';
import { Product } from './Product.js'; import { Product } from './Product.js';
import { connectToBrowser, BrowserConnectOptions } from './BrowserConnector.js'; import {
_connectToBrowser,
BrowserConnectOptions,
} from './BrowserConnector.js';
import { import {
PredefinedNetworkConditions, PredefinedNetworkConditions,
networkConditions, networkConditions,
@ -33,6 +36,7 @@ import {
/** /**
* Settings that are common to the Puppeteer class, regardless of environment. * Settings that are common to the Puppeteer class, regardless of environment.
*
* @internal * @internal
*/ */
export interface CommonPuppeteerSettings { export interface CommonPuppeteerSettings {
@ -85,7 +89,7 @@ export class Puppeteer {
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
connect(options: ConnectOptions): Promise<Browser> { connect(options: ConnectOptions): Promise<Browser> {
return connectToBrowser(options); return _connectToBrowser(options);
} }
/** /**
@ -110,7 +114,7 @@ export class Puppeteer {
* *
*/ */
get devices(): DevicesMap { get devices(): DevicesMap {
return devicesMap; return _devicesMap;
} }
/** /**
@ -182,27 +186,27 @@ export class Puppeteer {
name: string, name: string,
queryHandler: CustomQueryHandler queryHandler: CustomQueryHandler
): void { ): void {
registerCustomQueryHandler(name, queryHandler); _registerCustomQueryHandler(name, queryHandler);
} }
/** /**
* @param name - The name of the query handler to unregistered. * @param name - The name of the query handler to unregistered.
*/ */
unregisterCustomQueryHandler(name: string): void { unregisterCustomQueryHandler(name: string): void {
unregisterCustomQueryHandler(name); _unregisterCustomQueryHandler(name);
} }
/** /**
* @returns a list with the names of all registered custom query handlers. * @returns a list with the names of all registered custom query handlers.
*/ */
customQueryHandlerNames(): string[] { customQueryHandlerNames(): string[] {
return customQueryHandlerNames(); return _customQueryHandlerNames();
} }
/** /**
* Clears all registered handlers. * Clears all registered handlers.
*/ */
clearCustomQueryHandlers(): void { clearCustomQueryHandlers(): void {
clearCustomQueryHandlers(); _clearCustomQueryHandlers();
} }
} }

View File

@ -16,7 +16,7 @@
import { WaitForSelectorOptions, DOMWorld } from './DOMWorld.js'; import { WaitForSelectorOptions, DOMWorld } from './DOMWorld.js';
import { ElementHandle, JSHandle } from './JSHandle.js'; import { ElementHandle, JSHandle } from './JSHandle.js';
import { ariaHandler } from './AriaQueryHandler.js'; import { _ariaHandler } from './AriaQueryHandler.js';
/** /**
* @internal * @internal
@ -76,7 +76,7 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
domWorld: DOMWorld, domWorld: DOMWorld,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
) => domWorld.waitForSelectorInPage(queryOne, selector, options); ) => domWorld._waitForSelectorInPage(queryOne, selector, options);
} }
if (handler.queryAll) { if (handler.queryAll) {
@ -161,20 +161,20 @@ const pierceHandler = makeQueryHandler({
}, },
}); });
const _builtInHandlers = new Map([ const builtInHandlers = new Map([
['aria', ariaHandler], ['aria', _ariaHandler],
['pierce', pierceHandler], ['pierce', pierceHandler],
]); ]);
const _queryHandlers = new Map(_builtInHandlers); const queryHandlers = new Map(builtInHandlers);
/** /**
* @internal * @internal
*/ */
export function registerCustomQueryHandler( export function _registerCustomQueryHandler(
name: string, name: string,
handler: CustomQueryHandler handler: CustomQueryHandler
): void { ): void {
if (_queryHandlers.get(name)) if (queryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`); throw new Error(`A custom query handler named "${name}" already exists`);
const isValidName = /^[a-zA-Z]+$/.test(name); const isValidName = /^[a-zA-Z]+$/.test(name);
@ -183,38 +183,36 @@ export function registerCustomQueryHandler(
const internalHandler = makeQueryHandler(handler); const internalHandler = makeQueryHandler(handler);
_queryHandlers.set(name, internalHandler); queryHandlers.set(name, internalHandler);
} }
/** /**
* @internal * @internal
*/ */
export function unregisterCustomQueryHandler(name: string): void { export function _unregisterCustomQueryHandler(name: string): void {
if (_queryHandlers.has(name) && !_builtInHandlers.has(name)) { if (queryHandlers.has(name) && !builtInHandlers.has(name)) {
_queryHandlers.delete(name); queryHandlers.delete(name);
} }
} }
/** /**
* @internal * @internal
*/ */
export function customQueryHandlerNames(): string[] { export function _customQueryHandlerNames(): string[] {
return [..._queryHandlers.keys()].filter( return [...queryHandlers.keys()].filter((name) => !builtInHandlers.has(name));
(name) => !_builtInHandlers.has(name)
);
} }
/** /**
* @internal * @internal
*/ */
export function clearCustomQueryHandlers(): void { export function _clearCustomQueryHandlers(): void {
customQueryHandlerNames().forEach(unregisterCustomQueryHandler); _customQueryHandlerNames().forEach(_unregisterCustomQueryHandler);
} }
/** /**
* @internal * @internal
*/ */
export function getQueryHandlerAndSelector(selector: string): { export function _getQueryHandlerAndSelector(selector: string): {
updatedSelector: string; updatedSelector: string;
queryHandler: InternalQueryHandler; queryHandler: InternalQueryHandler;
} { } {
@ -225,7 +223,7 @@ export function getQueryHandlerAndSelector(selector: string): {
const index = selector.indexOf('/'); const index = selector.indexOf('/');
const name = selector.slice(0, index); const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1); const updatedSelector = selector.slice(index + 1);
const queryHandler = _queryHandlers.get(name); const queryHandler = queryHandlers.get(name);
if (!queryHandler) if (!queryHandler)
throw new Error( throw new Error(
`Query set to use "${name}", but no query handler of that name was found` `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 * @public
*/ */
export class SecurityDetails { export class SecurityDetails {
private _subjectName: string; #subjectName: string;
private _issuer: string; #issuer: string;
private _validFrom: number; #validFrom: number;
private _validTo: number; #validTo: number;
private _protocol: string; #protocol: string;
private _sanList: string[]; #sanList: string[];
/** /**
* @internal * @internal
*/ */
constructor(securityPayload: Protocol.Network.SecurityDetails) { constructor(securityPayload: Protocol.Network.SecurityDetails) {
this._subjectName = securityPayload.subjectName; this.#subjectName = securityPayload.subjectName;
this._issuer = securityPayload.issuer; this.#issuer = securityPayload.issuer;
this._validFrom = securityPayload.validFrom; this.#validFrom = securityPayload.validFrom;
this._validTo = securityPayload.validTo; this.#validTo = securityPayload.validTo;
this._protocol = securityPayload.protocol; this.#protocol = securityPayload.protocol;
this._sanList = securityPayload.sanList; this.#sanList = securityPayload.sanList;
} }
/** /**
* @returns The name of the issuer of the certificate. * @returns The name of the issuer of the certificate.
*/ */
issuer(): string { issuer(): string {
return this._issuer; return this.#issuer;
} }
/** /**
@ -54,7 +54,7 @@ export class SecurityDetails {
* marking the start of the certificate's validity. * marking the start of the certificate's validity.
*/ */
validFrom(): number { validFrom(): number {
return this._validFrom; return this.#validFrom;
} }
/** /**
@ -62,27 +62,27 @@ export class SecurityDetails {
* marking the end of the certificate's validity. * marking the end of the certificate's validity.
*/ */
validTo(): number { validTo(): number {
return this._validTo; return this.#validTo;
} }
/** /**
* @returns The security protocol being used, e.g. "TLS 1.2". * @returns The security protocol being used, e.g. "TLS 1.2".
*/ */
protocol(): string { protocol(): string {
return this._protocol; return this.#protocol;
} }
/** /**
* @returns The name of the subject to which the certificate was issued. * @returns The name of the subject to which the certificate was issued.
*/ */
subjectName(): string { 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. * @returns The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate.
*/ */
subjectAlternativeNames(): string[] { subjectAlternativeNames(): string[] {
return this._sanList; return this.#sanList;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -294,7 +294,7 @@ export type KeyInput =
/** /**
* @internal * @internal
*/ */
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = { export const _keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
'0': { keyCode: 48, key: '0', code: 'Digit0' }, '0': { keyCode: 48, key: '0', code: 'Digit0' },
'1': { keyCode: 49, key: '1', code: 'Digit1' }, '1': { keyCode: 49, key: '1', code: 'Digit1' },
'2': { keyCode: 50, key: '2', code: 'Digit2' }, '2': { keyCode: 50, key: '2', code: 'Digit2' },

View File

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

View File

@ -103,7 +103,7 @@ function archiveName(
/** /**
* @internal * @internal
*/ */
function downloadURL( function _downloadURL(
product: Product, product: Product,
platform: Platform, platform: Platform,
host: string, host: string,
@ -118,26 +118,24 @@ function downloadURL(
return url; return url;
} }
/**
* @internal
*/
function handleArm64(): void { function handleArm64(): void {
fs.stat('/usr/bin/chromium-browser', function (_err, stats) { let exists = fs.existsSync('/usr/bin/chromium-browser');
if (stats === undefined) { if (exists) {
fs.stat('/usr/bin/chromium', function (_err, stats) { return;
if (stats === undefined) { }
console.error( exists = fs.existsSync('/usr/bin/chromium');
'The chromium binary is not available for arm64.' + if (exists) {
'\nIf you are on Ubuntu, you can install with: ' + return;
'\n\n sudo apt install chromium\n' + }
'\n\n sudo apt install chromium-browser\n' console.error(
); 'The chromium binary is not available for arm64.' +
throw new Error(); '\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 readdirAsync = promisify(fs.readdir.bind(fs));
const mkdirAsync = promisify(fs.mkdir.bind(fs)); const mkdirAsync = promisify(fs.mkdir.bind(fs));
const unlinkAsync = promisify(fs.unlink.bind(fs)); const unlinkAsync = promisify(fs.unlink.bind(fs));
@ -195,49 +193,49 @@ export interface BrowserFetcherRevisionInfo {
*/ */
export class BrowserFetcher { export class BrowserFetcher {
private _product: Product; #product: Product;
private _downloadsFolder: string; #downloadsFolder: string;
private _downloadHost: string; #downloadHost: string;
private _platform: Platform; #platform: Platform;
/** /**
* @internal * @internal
*/ */
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) { constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
this._product = (options.product || 'chrome').toLowerCase() as Product; this.#product = (options.product || 'chrome').toLowerCase() as Product;
assert( assert(
this._product === 'chrome' || this._product === 'firefox', this.#product === 'chrome' || this.#product === 'firefox',
`Unknown product: "${options.product}"` `Unknown product: "${options.product}"`
); );
this._downloadsFolder = this.#downloadsFolder =
options.path || options.path ||
path.join(projectRoot, browserConfig[this._product].destination); path.join(projectRoot, browserConfig[this.#product].destination);
this._downloadHost = options.host || browserConfig[this._product].host; this.#downloadHost = options.host || browserConfig[this.#product].host;
if (options.platform) { if (options.platform) {
this._platform = options.platform; this.#platform = options.platform;
} else { } else {
const platform = os.platform(); const platform = os.platform();
switch (platform) { switch (platform) {
case 'darwin': case 'darwin':
switch (this._product) { switch (this.#product) {
case 'chrome': case 'chrome':
this._platform = this.#platform =
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
? 'mac_arm' ? 'mac_arm'
: 'mac'; : 'mac';
break; break;
case 'firefox': case 'firefox':
this._platform = 'mac'; this.#platform = 'mac';
break; break;
} }
break; break;
case 'linux': case 'linux':
this._platform = 'linux'; this.#platform = 'linux';
break; break;
case 'win32': case 'win32':
this._platform = os.arch() === 'x64' ? 'win64' : 'win32'; this.#platform = os.arch() === 'x64' ? 'win64' : 'win32';
return; return;
default: default:
assert(false, 'Unsupported platform: ' + platform); assert(false, 'Unsupported platform: ' + platform);
@ -245,8 +243,8 @@ export class BrowserFetcher {
} }
assert( assert(
downloadURLs[this._product][this._platform], downloadURLs[this.#product][this.#platform],
'Unsupported platform: ' + this._platform 'Unsupported platform: ' + this.#platform
); );
} }
@ -255,7 +253,7 @@ export class BrowserFetcher {
* `win32` or `win64`. * `win32` or `win64`.
*/ */
platform(): Platform { platform(): Platform {
return this._platform; return this.#platform;
} }
/** /**
@ -263,14 +261,14 @@ export class BrowserFetcher {
* `firefox`. * `firefox`.
*/ */
product(): Product { product(): Product {
return this._product; return this.#product;
} }
/** /**
* @returns The download host being used. * @returns The download host being used.
*/ */
host(): string { host(): string {
return this._downloadHost; return this.#downloadHost;
} }
/** /**
@ -282,10 +280,10 @@ export class BrowserFetcher {
* from the host. * from the host.
*/ */
canDownload(revision: string): Promise<boolean> { canDownload(revision: string): Promise<boolean> {
const url = downloadURL( const url = _downloadURL(
this._product, this.#product,
this._platform, this.#platform,
this._downloadHost, this.#downloadHost,
revision revision
); );
return new Promise((resolve) => { return new Promise((resolve) => {
@ -318,19 +316,19 @@ export class BrowserFetcher {
revision: string, revision: string,
progressCallback: (x: number, y: number) => void = (): void => {} progressCallback: (x: number, y: number) => void = (): void => {}
): Promise<BrowserFetcherRevisionInfo | undefined> { ): Promise<BrowserFetcherRevisionInfo | undefined> {
const url = downloadURL( const url = _downloadURL(
this._product, this.#product,
this._platform, this.#platform,
this._downloadHost, this.#downloadHost,
revision revision
); );
const fileName = url.split('/').pop(); const fileName = url.split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`); assert(fileName, `A malformed download URL was found: ${url}.`);
const archivePath = path.join(this._downloadsFolder, fileName); const archivePath = path.join(this.#downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision); const outputPath = this.#getFolderPath(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision); if (await existsAsync(outputPath)) return this.revisionInfo(revision);
if (!(await existsAsync(this._downloadsFolder))) if (!(await existsAsync(this.#downloadsFolder)))
await mkdirAsync(this._downloadsFolder); await mkdirAsync(this.#downloadsFolder);
// Use system Chromium builds on Linux ARM devices // Use system Chromium builds on Linux ARM devices
if (os.platform() !== 'darwin' && os.arch() === 'arm64') { if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
@ -338,7 +336,7 @@ export class BrowserFetcher {
return; return;
} }
try { try {
await downloadFile(url, archivePath, progressCallback); await _downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath); await install(archivePath, outputPath);
} finally { } finally {
if (await existsAsync(archivePath)) await unlinkAsync(archivePath); if (await existsAsync(archivePath)) await unlinkAsync(archivePath);
@ -355,15 +353,15 @@ export class BrowserFetcher {
* available locally on disk. * available locally on disk.
*/ */
async localRevisions(): Promise<string[]> { async localRevisions(): Promise<string[]> {
if (!(await existsAsync(this._downloadsFolder))) return []; if (!(await existsAsync(this.#downloadsFolder))) return [];
const fileNames = await readdirAsync(this._downloadsFolder); const fileNames = await readdirAsync(this.#downloadsFolder);
return fileNames return fileNames
.map((fileName) => parseFolderPath(this._product, fileName)) .map((fileName) => parseFolderPath(this.#product, fileName))
.filter( .filter(
( (
entry entry
): entry is { product: string; platform: string; revision: string } => ): entry is { product: string; platform: string; revision: string } =>
(entry && entry.platform === this._platform) ?? false (entry && entry.platform === this.#platform) ?? false
) )
.map((entry) => entry.revision); .map((entry) => entry.revision);
} }
@ -376,7 +374,7 @@ export class BrowserFetcher {
* throws if the revision has not been downloaded. * throws if the revision has not been downloaded.
*/ */
async remove(revision: string): Promise<void> { async remove(revision: string): Promise<void> {
const folderPath = this._getFolderPath(revision); const folderPath = this.#getFolderPath(revision);
assert( assert(
await existsAsync(folderPath), await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded` `Failed to remove: revision ${revision} is not downloaded`
@ -389,33 +387,33 @@ export class BrowserFetcher {
* @returns The revision info for the given revision. * @returns The revision info for the given revision.
*/ */
revisionInfo(revision: string): BrowserFetcherRevisionInfo { revisionInfo(revision: string): BrowserFetcherRevisionInfo {
const folderPath = this._getFolderPath(revision); const folderPath = this.#getFolderPath(revision);
let executablePath = ''; let executablePath = '';
if (this._product === 'chrome') { if (this.#product === 'chrome') {
if (this._platform === 'mac' || this._platform === 'mac_arm') if (this.#platform === 'mac' || this.#platform === 'mac_arm')
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this._product, this._platform, revision), archiveName(this.#product, this.#platform, revision),
'Chromium.app', 'Chromium.app',
'Contents', 'Contents',
'MacOS', 'MacOS',
'Chromium' 'Chromium'
); );
else if (this._platform === 'linux') else if (this.#platform === 'linux')
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this._product, this._platform, revision), archiveName(this.#product, this.#platform, revision),
'chrome' 'chrome'
); );
else if (this._platform === 'win32' || this._platform === 'win64') else if (this.#platform === 'win32' || this.#platform === 'win64')
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this._product, this._platform, revision), archiveName(this.#product, this.#platform, revision),
'chrome.exe' 'chrome.exe'
); );
else throw new Error('Unsupported platform: ' + this._platform); else throw new Error('Unsupported platform: ' + this.#platform);
} else if (this._product === 'firefox') { } else if (this.#product === 'firefox') {
if (this._platform === 'mac' || this._platform === 'mac_arm') if (this.#platform === 'mac' || this.#platform === 'mac_arm')
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
'Firefox Nightly.app', 'Firefox Nightly.app',
@ -423,16 +421,16 @@ export class BrowserFetcher {
'MacOS', 'MacOS',
'firefox' 'firefox'
); );
else if (this._platform === 'linux') else if (this.#platform === 'linux')
executablePath = path.join(folderPath, 'firefox', 'firefox'); 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'); executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else throw new Error('Unsupported platform: ' + this._platform); else throw new Error('Unsupported platform: ' + this.#platform);
} else throw new Error('Unsupported product: ' + this._product); } else throw new Error('Unsupported product: ' + this.#product);
const url = downloadURL( const url = _downloadURL(
this._product, this.#product,
this._platform, this.#platform,
this._downloadHost, this.#downloadHost,
revision revision
); );
const local = fs.existsSync(folderPath); const local = fs.existsSync(folderPath);
@ -442,7 +440,7 @@ export class BrowserFetcher {
folderPath, folderPath,
local, local,
url, url,
product: this._product, product: this.#product,
}); });
return { return {
revision, revision,
@ -450,15 +448,12 @@ export class BrowserFetcher {
folderPath, folderPath,
local, local,
url, url,
product: this._product, product: this.#product,
}; };
} }
/** #getFolderPath(revision: string): string {
* @internal 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 * @internal
*/ */
function downloadFile( function _downloadFile(
url: string, url: string,
destinationPath: string, destinationPath: string,
progressCallback?: (x: number, y: number) => void progressCallback?: (x: number, y: number) => void
@ -524,10 +519,10 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
if (archivePath.endsWith('.zip')) if (archivePath.endsWith('.zip'))
return extractZip(archivePath, { dir: folderPath }); return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2')) else if (archivePath.endsWith('.tar.bz2'))
return extractTar(archivePath, folderPath); return _extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg')) else if (archivePath.endsWith('.dmg'))
return mkdirAsync(folderPath).then(() => return mkdirAsync(folderPath).then(() =>
installDMG(archivePath, folderPath) _installDMG(archivePath, folderPath)
); );
else throw new Error(`Unsupported archive format: ${archivePath}`); else throw new Error(`Unsupported archive format: ${archivePath}`);
} }
@ -535,7 +530,7 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
/** /**
* @internal * @internal
*/ */
function extractTar(tarPath: string, folderPath: string): Promise<unknown> { function _extractTar(tarPath: string, folderPath: string): Promise<unknown> {
return new Promise((fulfill, reject) => { return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath); const tarStream = tar.extract(folderPath);
tarStream.on('error', reject); tarStream.on('error', reject);
@ -548,7 +543,7 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
/** /**
* @internal * @internal
*/ */
function installDMG(dmgPath: string, folderPath: string): Promise<void> { function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
let mountPath: string | undefined; let mountPath: string | undefined;
return new Promise<void>((fulfill, reject): void => { 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.`; If you think this is a bug, please report it on the Puppeteer issue tracker.`;
export class BrowserRunner { export class BrowserRunner {
private _product: Product; #product: Product;
private _executablePath: string; #executablePath: string;
private _processArguments: string[]; #processArguments: string[];
private _userDataDir: string; #userDataDir: string;
private _isTempUserDataDir?: boolean; #isTempUserDataDir?: boolean;
#closed = true;
#listeners: PuppeteerEventListener[] = [];
#processClosing!: Promise<void>;
proc?: childProcess.ChildProcess; proc?: childProcess.ChildProcess;
connection?: Connection; connection?: Connection;
private _closed = true;
private _listeners: PuppeteerEventListener[] = [];
private _processClosing!: Promise<void>;
constructor( constructor(
product: Product, product: Product,
executablePath: string, executablePath: string,
@ -70,11 +69,11 @@ export class BrowserRunner {
userDataDir: string, userDataDir: string,
isTempUserDataDir?: boolean isTempUserDataDir?: boolean
) { ) {
this._product = product; this.#product = product;
this._executablePath = executablePath; this.#executablePath = executablePath;
this._processArguments = processArguments; this.#processArguments = processArguments;
this._userDataDir = userDataDir; this.#userDataDir = userDataDir;
this._isTempUserDataDir = isTempUserDataDir; this.#isTempUserDataDir = isTempUserDataDir;
} }
start(options: LaunchOptions): void { start(options: LaunchOptions): void {
@ -90,11 +89,11 @@ export class BrowserRunner {
} }
assert(!this.proc, 'This process has previously been started.'); assert(!this.proc, 'This process has previously been started.');
debugLauncher( debugLauncher(
`Calling ${this._executablePath} ${this._processArguments.join(' ')}` `Calling ${this.#executablePath} ${this.#processArguments.join(' ')}`
); );
this.proc = childProcess.spawn( this.proc = childProcess.spawn(
this._executablePath, this.#executablePath,
this._processArguments, this.#processArguments,
{ {
// On non-windows platforms, `detached: true` makes child process a // On non-windows platforms, `detached: true` makes child process a
// leader of a new process group, making it possible to kill child // 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.stderr?.pipe(process.stderr);
this.proc.stdout?.pipe(process.stdout); this.proc.stdout?.pipe(process.stdout);
} }
this._closed = false; this.#closed = false;
this._processClosing = new Promise((fulfill, reject) => { this.#processClosing = new Promise((fulfill, reject) => {
this.proc!.once('exit', async () => { this.proc!.once('exit', async () => {
this._closed = true; this.#closed = true;
// Cleanup as processes exit. // Cleanup as processes exit.
if (this._isTempUserDataDir) { if (this.#isTempUserDataDir) {
try { try {
await removeFolderAsync(this._userDataDir); await removeFolderAsync(this.#userDataDir);
fulfill(); fulfill();
} catch (error) { } catch (error) {
debugError(error); debugError(error);
reject(error); reject(error);
} }
} else { } else {
if (this._product === 'firefox') { if (this.#product === 'firefox') {
try { try {
// When an existing user profile has been used remove the user // When an existing user profile has been used remove the user
// preferences file and restore possibly backuped preferences. // 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( const prefsBackupPath = path.join(
this._userDataDir, this.#userDataDir,
'prefs.js.puppeteer' 'prefs.js.puppeteer'
); );
if (fs.existsSync(prefsBackupPath)) { if (fs.existsSync(prefsBackupPath)) {
const prefsPath = path.join(this._userDataDir, 'prefs.js'); const prefsPath = path.join(this.#userDataDir, 'prefs.js');
await unlinkAsync(prefsPath); await unlinkAsync(prefsPath);
await renameAsync(prefsBackupPath, prefsPath); await renameAsync(prefsBackupPath, prefsPath);
} }
@ -148,29 +147,29 @@ export class BrowserRunner {
} }
}); });
}); });
this._listeners = [ this.#listeners = [
helper.addEventListener(process, 'exit', this.kill.bind(this)), helper.addEventListener(process, 'exit', this.kill.bind(this)),
]; ];
if (handleSIGINT) if (handleSIGINT)
this._listeners.push( this.#listeners.push(
helper.addEventListener(process, 'SIGINT', () => { helper.addEventListener(process, 'SIGINT', () => {
this.kill(); this.kill();
process.exit(130); process.exit(130);
}) })
); );
if (handleSIGTERM) if (handleSIGTERM)
this._listeners.push( this.#listeners.push(
helper.addEventListener(process, 'SIGTERM', this.close.bind(this)) helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
); );
if (handleSIGHUP) if (handleSIGHUP)
this._listeners.push( this.#listeners.push(
helper.addEventListener(process, 'SIGHUP', this.close.bind(this)) helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
); );
} }
close(): Promise<void> { close(): Promise<void> {
if (this._closed) return Promise.resolve(); if (this.#closed) return Promise.resolve();
if (this._isTempUserDataDir) { if (this.#isTempUserDataDir) {
this.kill(); this.kill();
} else if (this.connection) { } else if (this.connection) {
// Attempt to close the browser gracefully // 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 // 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. // perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners); helper.removeEventListeners(this.#listeners);
return this._processClosing; return this.#processClosing;
} }
kill(): void { kill(): void {
@ -226,14 +225,14 @@ export class BrowserRunner {
// Attempt to remove temporary profile directory to avoid littering. // Attempt to remove temporary profile directory to avoid littering.
try { try {
if (this._isTempUserDataDir) { if (this.#isTempUserDataDir) {
removeFolder.sync(this._userDataDir); removeFolder.sync(this.#userDataDir);
} }
} catch (error) {} } catch (error) {}
// Cleanup this listener last, as that makes sure the full callback runs. If we // 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. // perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners); helper.removeEventListeners(this.#listeners);
} }
async setupConnection(options: { async setupConnection(options: {

View File

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

View File

@ -50,27 +50,27 @@ export class NodeWebSocketTransport implements ConnectionTransport {
}); });
} }
private _ws: NodeWebSocket; #ws: NodeWebSocket;
onmessage?: (message: NodeWebSocket.Data) => void; onmessage?: (message: NodeWebSocket.Data) => void;
onclose?: () => void; onclose?: () => void;
constructor(ws: NodeWebSocket) { constructor(ws: NodeWebSocket) {
this._ws = ws; this.#ws = ws;
this._ws.addEventListener('message', (event) => { this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data); if (this.onmessage) this.onmessage.call(null, event.data);
}); });
this._ws.addEventListener('close', () => { this.#ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null); if (this.onclose) this.onclose.call(null);
}); });
// Silently ignore all errors - we don't know what to do with them. // 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 { send(message: string): void {
this._ws.send(message); this.#ws.send(message);
} }
close(): void { 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'; import { assert } from '../common/assert.js';
export class PipeTransport implements ConnectionTransport { export class PipeTransport implements ConnectionTransport {
_pipeWrite: NodeJS.WritableStream; #pipeWrite: NodeJS.WritableStream;
_eventListeners: PuppeteerEventListener[]; #eventListeners: PuppeteerEventListener[];
_isClosed = false; #isClosed = false;
_pendingMessage = ''; #pendingMessage = '';
onclose?: () => void; onclose?: () => void;
onmessage?: (value: string) => void; onmessage?: (value: string) => void;
@ -35,10 +35,10 @@ export class PipeTransport implements ConnectionTransport {
pipeWrite: NodeJS.WritableStream, pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream pipeRead: NodeJS.ReadableStream
) { ) {
this._pipeWrite = pipeWrite; this.#pipeWrite = pipeWrite;
this._eventListeners = [ this.#eventListeners = [
helper.addEventListener(pipeRead, 'data', (buffer) => helper.addEventListener(pipeRead, 'data', (buffer) =>
this._dispatch(buffer) this.#dispatch(buffer)
), ),
helper.addEventListener(pipeRead, 'close', () => { helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose) { if (this.onclose) {
@ -51,21 +51,21 @@ export class PipeTransport implements ConnectionTransport {
} }
send(message: string): void { send(message: string): void {
assert(!this._isClosed, '`PipeTransport` is closed.'); assert(!this.#isClosed, '`PipeTransport` is closed.');
this._pipeWrite.write(message); this.#pipeWrite.write(message);
this._pipeWrite.write('\0'); this.#pipeWrite.write('\0');
} }
_dispatch(buffer: Buffer): void { #dispatch(buffer: Buffer): void {
assert(!this._isClosed, '`PipeTransport` is closed.'); assert(!this.#isClosed, '`PipeTransport` is closed.');
let end = buffer.indexOf('\0'); let end = buffer.indexOf('\0');
if (end === -1) { if (end === -1) {
this._pendingMessage += buffer.toString(); this.#pendingMessage += buffer.toString();
return; return;
} }
const message = this._pendingMessage + buffer.toString(undefined, 0, end); const message = this.#pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage) { if (this.onmessage) {
this.onmessage.call(null, message); this.onmessage.call(null, message);
} }
@ -79,11 +79,11 @@ export class PipeTransport implements ConnectionTransport {
start = end + 1; start = end + 1;
end = buffer.indexOf('\0', start); end = buffer.indexOf('\0', start);
} }
this._pendingMessage = buffer.toString(undefined, start); this.#pendingMessage = buffer.toString(undefined, start);
} }
close(): void { close(): void {
this._isClosed = true; this.#isClosed = true;
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this.#eventListeners);
} }
} }

View File

@ -77,12 +77,10 @@ export interface PuppeteerLaunchOptions
* @public * @public
*/ */
export class PuppeteerNode extends Puppeteer { export class PuppeteerNode extends Puppeteer {
private _lazyLauncher?: ProductLauncher; #lazyLauncher?: ProductLauncher;
private _projectRoot?: string; #projectRoot?: string;
private __productName?: Product; #productName?: Product;
/**
* @internal
*/
_preferredRevision: string; _preferredRevision: string;
/** /**
@ -98,8 +96,8 @@ export class PuppeteerNode extends Puppeteer {
const { projectRoot, preferredRevision, productName, ...commonSettings } = const { projectRoot, preferredRevision, productName, ...commonSettings } =
settings; settings;
super(commonSettings); super(commonSettings);
this._projectRoot = projectRoot; this.#projectRoot = projectRoot;
this.__productName = productName; this.#productName = productName;
this._preferredRevision = preferredRevision; this._preferredRevision = preferredRevision;
this.connect = this.connect.bind(this); this.connect = this.connect.bind(this);
@ -126,13 +124,15 @@ export class PuppeteerNode extends Puppeteer {
* @internal * @internal
*/ */
get _productName(): Product | undefined { 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) { set _productName(name: Product | undefined) {
if (this.__productName !== name) this._changedProduct = true; if (this.#productName !== name) this._changedProduct = true;
this.__productName = name; this.#productName = name;
} }
/** /**
@ -184,8 +184,8 @@ export class PuppeteerNode extends Puppeteer {
*/ */
get _launcher(): ProductLauncher { get _launcher(): ProductLauncher {
if ( if (
!this._lazyLauncher || !this.#lazyLauncher ||
this._lazyLauncher.product !== this._productName || this.#lazyLauncher.product !== this._productName ||
this._changedProduct this._changedProduct
) { ) {
switch (this._productName) { switch (this._productName) {
@ -197,14 +197,14 @@ export class PuppeteerNode extends Puppeteer {
this._preferredRevision = PUPPETEER_REVISIONS.chromium; this._preferredRevision = PUPPETEER_REVISIONS.chromium;
} }
this._changedProduct = false; this._changedProduct = false;
this._lazyLauncher = Launcher( this.#lazyLauncher = Launcher(
this._projectRoot, this.#projectRoot,
this._preferredRevision, this._preferredRevision,
this._isPuppeteerCore, this._isPuppeteerCore,
this._productName this._productName
); );
} }
return this._lazyLauncher; return this.#lazyLauncher;
} }
/** /**
@ -234,11 +234,11 @@ export class PuppeteerNode extends Puppeteer {
* @returns A new BrowserFetcher instance. * @returns A new BrowserFetcher instance.
*/ */
createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher { createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher {
if (!this._projectRoot) { if (!this.#projectRoot) {
throw new Error( throw new Error(
'_projectRoot is undefined. Unable to create a BrowserFetcher.' '_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(); const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = ''; let dumpioData = '';
const { spawn } = require('child_process'); const { spawn } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, { const options = Object.assign({}, defaultBrowserOptions, {
pipe: true, pipe: true,
dumpio: true, dumpio: true,
@ -44,7 +44,7 @@ describe('Fixtures', function () {
const { defaultBrowserOptions, puppeteerPath } = getTestState(); const { defaultBrowserOptions, puppeteerPath } = getTestState();
let dumpioData = ''; let dumpioData = '';
const { spawn } = require('child_process'); const { spawn } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, { dumpio: true }); const options = Object.assign({}, defaultBrowserOptions, { dumpio: true });
const res = spawn('node', [ const res = spawn('node', [
path.join(__dirname, 'fixtures', 'dumpio.js'), path.join(__dirname, 'fixtures', 'dumpio.js'),
@ -58,7 +58,7 @@ describe('Fixtures', function () {
it('should close the browser when the node process closes', async () => { it('should close the browser when the node process closes', async () => {
const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState(); const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState();
const { spawn, execSync } = require('child_process'); const { spawn, execSync } = await import('child_process');
const options = Object.assign({}, defaultBrowserOptions, { const options = Object.assign({}, defaultBrowserOptions, {
// Disable DUMPIO to cleanly read stdout. // Disable DUMPIO to cleanly read stdout.
dumpio: false, dumpio: false,

View File

@ -292,7 +292,7 @@ describe('Frame specs', function () {
describe('Frame.client', function () { describe('Frame.client', function () {
it('should return the client instance', async () => { it('should return the client instance', async () => {
const { page } = getTestState(); 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({ const browser = await puppeteer.connect({
browserWSEndpoint, browserWSEndpoint,
isPageTarget: (target) => { _isPageTarget(target) {
return ( return (
target.type === 'other' && target.url.startsWith('devtools://') 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); const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle);
expect(isFive).toBeTruthy(); expect(isFive).toBeTruthy();
}); });
it('should warn on nested object handles', async () => { it('should warn about recursive objects', async () => {
const { page } = getTestState(); const { page } = getTestState();
const aHandle = await page.evaluateHandle(() => document.body); const test: { obj?: unknown } = {};
test.obj = test;
let error = null; let error = null;
await page await page
// @ts-expect-error we are deliberately passing a bad type here (nested object) // @ts-expect-error we are deliberately passing a bad type here (nested object)
.evaluateHandle((opts) => opts.elem.querySelector('p'), { .evaluateHandle((opts) => opts.elem, { test })
elem: aHandle,
})
.catch((error_) => (error = error_)); .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 () => { it('should accept object handle to unserializable value', async () => {
const { page } = getTestState(); const { page } = getTestState();

View File

@ -21,12 +21,17 @@ import {
describeChromeOnly, describeChromeOnly,
itFailsFirefox, itFailsFirefox,
} from './mocha-utils'; // eslint-disable-line import/extensions } from './mocha-utils'; // eslint-disable-line import/extensions
import {
Browser,
BrowserContext,
Page,
} from '../lib/cjs/puppeteer/api-docs-entry.js';
describeChromeOnly('OOPIF', function () { describeChromeOnly('OOPIF', function () {
/* We use a special browser for this test as we need the --site-per-process flag */ /* We use a special browser for this test as we need the --site-per-process flag */
let browser; let browser: Browser;
let context; let context: BrowserContext;
let page; let page: Page;
before(async () => { before(async () => {
const { puppeteer, defaultBrowserOptions } = getTestState(); const { puppeteer, defaultBrowserOptions } = getTestState();
@ -433,8 +438,8 @@ describeChromeOnly('OOPIF', function () {
}); });
}); });
function oopifs(context) { function oopifs(context: BrowserContext) {
return context return context
.targets() .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 () { describe('Page.client', function () {
it('should return the client instance', async () => { it('should return the client instance', async () => {
const { page } = getTestState(); const { page } = getTestState();
expect(page.client()).toBeInstanceOf(CDPSession); expect(page._client()).toBeInstanceOf(CDPSession);
}); });
}); });
}); });