chore: use private fields (#8506)
This commit is contained in:
parent
733cbecf48
commit
6c960115a3
37
.eslintrc.js
37
.eslintrc.js
@ -1,3 +1,12 @@
|
||||
// TODO: Enable this at some point.
|
||||
// const RESTRICTED_UNDERSCORED_IDENTIFIERS = [
|
||||
// 'PropertyDefinition > Identifier[name=/^_[a-z].*$/]',
|
||||
// ].map((selector) => ({
|
||||
// selector,
|
||||
// message:
|
||||
// 'Use private fields (fields prefixed with #) and an appropriate getter/setter.',
|
||||
// }));
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
@ -15,14 +24,6 @@ module.exports = {
|
||||
// Error if files are not formatted with Prettier correctly.
|
||||
'prettier/prettier': 2,
|
||||
// syntax preferences
|
||||
quotes: [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'spaced-comment': [
|
||||
2,
|
||||
'always',
|
||||
@ -116,6 +117,12 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/extensions': ['error', 'ignorePackages'],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
// Don't allow underscored declarations on camelCased variables/properties.
|
||||
// ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@ -144,8 +151,6 @@ module.exports = {
|
||||
// We don't require explicit return types on basic functions or
|
||||
// dummy functions in tests, for example
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
// We know it's bad and use it very sparingly but it's needed :(
|
||||
'@typescript-eslint/ban-ts-ignore': 0,
|
||||
// We allow non-null assertions if the value was asserted using `assert` API.
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
/**
|
||||
@ -176,6 +181,18 @@ module.exports = {
|
||||
],
|
||||
// By default this is a warning but we want it to error.
|
||||
'@typescript-eslint/explicit-module-boundary-types': 2,
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
// Never use `require` in TypeScript since they are transpiled out.
|
||||
selector: "CallExpression[callee.name='require']",
|
||||
message: '`require` statements are not allowed. Use `import`.',
|
||||
},
|
||||
|
||||
// Don't allow underscored declarations on camelCased variables/properties.
|
||||
// ...RESTRICTED_UNDERSCORED_IDENTIFIERS,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -130,13 +130,13 @@ export interface SnapshotOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Accessibility {
|
||||
private _client: CDPSession;
|
||||
#client: CDPSession;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,10 +182,10 @@ export class Accessibility {
|
||||
options: SnapshotOptions = {}
|
||||
): Promise<SerializedAXNode | null> {
|
||||
const { interestingOnly = true, root = null } = options;
|
||||
const { nodes } = await this._client.send('Accessibility.getFullAXTree');
|
||||
const { nodes } = await this.#client.send('Accessibility.getFullAXTree');
|
||||
let backendNodeId: number | undefined;
|
||||
if (root) {
|
||||
const { node } = await this._client.send('DOM.describeNode', {
|
||||
const { node } = await this.#client.send('DOM.describeNode', {
|
||||
objectId: root._remoteObject.objectId,
|
||||
});
|
||||
backendNodeId = node.backendNodeId;
|
||||
@ -238,53 +238,53 @@ class AXNode {
|
||||
public payload: Protocol.Accessibility.AXNode;
|
||||
public children: AXNode[] = [];
|
||||
|
||||
private _richlyEditable = false;
|
||||
private _editable = false;
|
||||
private _focusable = false;
|
||||
private _hidden = false;
|
||||
private _name: string;
|
||||
private _role: string;
|
||||
private _ignored: boolean;
|
||||
private _cachedHasFocusableChild?: boolean;
|
||||
#richlyEditable = false;
|
||||
#editable = false;
|
||||
#focusable = false;
|
||||
#hidden = false;
|
||||
#name: string;
|
||||
#role: string;
|
||||
#ignored: boolean;
|
||||
#cachedHasFocusableChild?: boolean;
|
||||
|
||||
constructor(payload: Protocol.Accessibility.AXNode) {
|
||||
this.payload = payload;
|
||||
this._name = this.payload.name ? this.payload.name.value : '';
|
||||
this._role = this.payload.role ? this.payload.role.value : 'Unknown';
|
||||
this._ignored = this.payload.ignored;
|
||||
this.#name = this.payload.name ? this.payload.name.value : '';
|
||||
this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
|
||||
this.#ignored = this.payload.ignored;
|
||||
|
||||
for (const property of this.payload.properties || []) {
|
||||
if (property.name === 'editable') {
|
||||
this._richlyEditable = property.value.value === 'richtext';
|
||||
this._editable = true;
|
||||
this.#richlyEditable = property.value.value === 'richtext';
|
||||
this.#editable = true;
|
||||
}
|
||||
if (property.name === 'focusable') this._focusable = property.value.value;
|
||||
if (property.name === 'hidden') this._hidden = property.value.value;
|
||||
if (property.name === 'focusable') this.#focusable = property.value.value;
|
||||
if (property.name === 'hidden') this.#hidden = property.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
private _isPlainTextField(): boolean {
|
||||
if (this._richlyEditable) return false;
|
||||
if (this._editable) return true;
|
||||
return this._role === 'textbox' || this._role === 'searchbox';
|
||||
#isPlainTextField(): boolean {
|
||||
if (this.#richlyEditable) return false;
|
||||
if (this.#editable) return true;
|
||||
return this.#role === 'textbox' || this.#role === 'searchbox';
|
||||
}
|
||||
|
||||
private _isTextOnlyObject(): boolean {
|
||||
const role = this._role;
|
||||
#isTextOnlyObject(): boolean {
|
||||
const role = this.#role;
|
||||
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
|
||||
}
|
||||
|
||||
private _hasFocusableChild(): boolean {
|
||||
if (this._cachedHasFocusableChild === undefined) {
|
||||
this._cachedHasFocusableChild = false;
|
||||
#hasFocusableChild(): boolean {
|
||||
if (this.#cachedHasFocusableChild === undefined) {
|
||||
this.#cachedHasFocusableChild = false;
|
||||
for (const child of this.children) {
|
||||
if (child._focusable || child._hasFocusableChild()) {
|
||||
this._cachedHasFocusableChild = true;
|
||||
if (child.#focusable || child.#hasFocusableChild()) {
|
||||
this.#cachedHasFocusableChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._cachedHasFocusableChild;
|
||||
return this.#cachedHasFocusableChild;
|
||||
}
|
||||
|
||||
public find(predicate: (x: AXNode) => boolean): AXNode | null {
|
||||
@ -303,13 +303,13 @@ class AXNode {
|
||||
// implementation details, but we want to expose them as leaves to platform
|
||||
// accessibility APIs because screen readers might be confused if they find
|
||||
// any children.
|
||||
if (this._isPlainTextField() || this._isTextOnlyObject()) return true;
|
||||
if (this.#isPlainTextField() || this.#isTextOnlyObject()) return true;
|
||||
|
||||
// Roles whose children are only presentational according to the ARIA and
|
||||
// HTML5 Specs should be hidden from screen readers.
|
||||
// (Note that whilst ARIA buttons can have only presentational children, HTML5
|
||||
// buttons are allowed to have content.)
|
||||
switch (this._role) {
|
||||
switch (this.#role) {
|
||||
case 'doc-cover':
|
||||
case 'graphics-symbol':
|
||||
case 'img':
|
||||
@ -324,14 +324,14 @@ class AXNode {
|
||||
}
|
||||
|
||||
// Here and below: Android heuristics
|
||||
if (this._hasFocusableChild()) return false;
|
||||
if (this._focusable && this._name) return true;
|
||||
if (this._role === 'heading' && this._name) return true;
|
||||
if (this.#hasFocusableChild()) return false;
|
||||
if (this.#focusable && this.#name) return true;
|
||||
if (this.#role === 'heading' && this.#name) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public isControl(): boolean {
|
||||
switch (this._role) {
|
||||
switch (this.#role) {
|
||||
case 'button':
|
||||
case 'checkbox':
|
||||
case 'ColorWell':
|
||||
@ -360,10 +360,10 @@ class AXNode {
|
||||
}
|
||||
|
||||
public isInteresting(insideControl: boolean): boolean {
|
||||
const role = this._role;
|
||||
if (role === 'Ignored' || this._hidden || this._ignored) return false;
|
||||
const role = this.#role;
|
||||
if (role === 'Ignored' || this.#hidden || this.#ignored) return false;
|
||||
|
||||
if (this._focusable || this._richlyEditable) return true;
|
||||
if (this.#focusable || this.#richlyEditable) return true;
|
||||
|
||||
// If it's not focusable but has a control role, then it's interesting.
|
||||
if (this.isControl()) return true;
|
||||
@ -371,7 +371,7 @@ class AXNode {
|
||||
// A non focusable child of a control is not interesting
|
||||
if (insideControl) return false;
|
||||
|
||||
return this.isLeafNode() && !!this._name;
|
||||
return this.isLeafNode() && !!this.#name;
|
||||
}
|
||||
|
||||
public serialize(): SerializedAXNode {
|
||||
@ -384,7 +384,7 @@ class AXNode {
|
||||
properties.set('description', this.payload.description.value);
|
||||
|
||||
const node: SerializedAXNode = {
|
||||
role: this._role,
|
||||
role: this.#role,
|
||||
};
|
||||
|
||||
type UserStringProperty =
|
||||
@ -440,7 +440,7 @@ class AXNode {
|
||||
// RootWebArea's treat focus differently than other nodes. They report whether
|
||||
// their frame has focus, not whether focus is specifically on the root
|
||||
// node.
|
||||
if (booleanProperty === 'focused' && this._role === 'RootWebArea')
|
||||
if (booleanProperty === 'focused' && this.#role === 'RootWebArea')
|
||||
continue;
|
||||
const value = getBooleanPropertyValue(booleanProperty);
|
||||
if (!value) continue;
|
||||
|
@ -107,7 +107,7 @@ const waitFor = async (
|
||||
return element;
|
||||
},
|
||||
};
|
||||
return domWorld.waitForSelectorInPage(
|
||||
return domWorld._waitForSelectorInPage(
|
||||
(_: Element, selector: string) =>
|
||||
(
|
||||
globalThis as unknown as { ariaQuerySelector(selector: string): void }
|
||||
@ -146,7 +146,7 @@ const queryAllArray = async (
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const ariaHandler: InternalQueryHandler = {
|
||||
export const _ariaHandler: InternalQueryHandler = {
|
||||
queryOne,
|
||||
waitFor,
|
||||
queryAll,
|
||||
|
@ -217,7 +217,7 @@ export class Browser extends EventEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static async create(
|
||||
static async _create(
|
||||
connection: Connection,
|
||||
contextIds: string[],
|
||||
ignoreHTTPSErrors: boolean,
|
||||
@ -240,22 +240,25 @@ export class Browser extends EventEmitter {
|
||||
await connection.send('Target.setDiscoverTargets', { discover: true });
|
||||
return browser;
|
||||
}
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport?: Viewport | null;
|
||||
private _process?: ChildProcess;
|
||||
private _connection: Connection;
|
||||
private _closeCallback: BrowserCloseCallback;
|
||||
private _targetFilterCallback: TargetFilterCallback;
|
||||
private _isPageTargetCallback!: IsPageTargetCallback;
|
||||
private _defaultContext: BrowserContext;
|
||||
private _contexts: Map<string, BrowserContext>;
|
||||
private _screenshotTaskQueue: TaskQueue;
|
||||
private _ignoredTargets = new Set<string>();
|
||||
#ignoreHTTPSErrors: boolean;
|
||||
#defaultViewport?: Viewport | null;
|
||||
#process?: ChildProcess;
|
||||
#connection: Connection;
|
||||
#closeCallback: BrowserCloseCallback;
|
||||
#targetFilterCallback: TargetFilterCallback;
|
||||
#isPageTargetCallback!: IsPageTargetCallback;
|
||||
#defaultContext: BrowserContext;
|
||||
#contexts: Map<string, BrowserContext>;
|
||||
#screenshotTaskQueue: TaskQueue;
|
||||
#targets: Map<string, Target>;
|
||||
#ignoredTargets = new Set<string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Used in Target.ts directly so cannot be marked private.
|
||||
*/
|
||||
_targets: Map<string, Target>;
|
||||
get _targets(): Map<string, Target> {
|
||||
return this.#targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -271,35 +274,35 @@ export class Browser extends EventEmitter {
|
||||
isPageTargetCallback?: IsPageTargetCallback
|
||||
) {
|
||||
super();
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport;
|
||||
this._process = process;
|
||||
this._screenshotTaskQueue = new TaskQueue();
|
||||
this._connection = connection;
|
||||
this._closeCallback = closeCallback || function (): void {};
|
||||
this._targetFilterCallback = targetFilterCallback || ((): boolean => true);
|
||||
this._setIsPageTargetCallback(isPageTargetCallback);
|
||||
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this.#defaultViewport = defaultViewport;
|
||||
this.#process = process;
|
||||
this.#screenshotTaskQueue = new TaskQueue();
|
||||
this.#connection = connection;
|
||||
this.#closeCallback = closeCallback || function (): void {};
|
||||
this.#targetFilterCallback = targetFilterCallback || ((): boolean => true);
|
||||
this.#setIsPageTargetCallback(isPageTargetCallback);
|
||||
|
||||
this._defaultContext = new BrowserContext(this._connection, this);
|
||||
this._contexts = new Map();
|
||||
this.#defaultContext = new BrowserContext(this.#connection, this);
|
||||
this.#contexts = new Map();
|
||||
for (const contextId of contextIds)
|
||||
this._contexts.set(
|
||||
this.#contexts.set(
|
||||
contextId,
|
||||
new BrowserContext(this._connection, this, contextId)
|
||||
new BrowserContext(this.#connection, this, contextId)
|
||||
);
|
||||
|
||||
this._targets = new Map();
|
||||
this._connection.on(ConnectionEmittedEvents.Disconnected, () =>
|
||||
this.#targets = new Map();
|
||||
this.#connection.on(ConnectionEmittedEvents.Disconnected, () =>
|
||||
this.emit(BrowserEmittedEvents.Disconnected)
|
||||
);
|
||||
this._connection.on('Target.targetCreated', this._targetCreated.bind(this));
|
||||
this._connection.on(
|
||||
this.#connection.on('Target.targetCreated', this.#targetCreated.bind(this));
|
||||
this.#connection.on(
|
||||
'Target.targetDestroyed',
|
||||
this._targetDestroyed.bind(this)
|
||||
this.#targetDestroyed.bind(this)
|
||||
);
|
||||
this._connection.on(
|
||||
this.#connection.on(
|
||||
'Target.targetInfoChanged',
|
||||
this._targetInfoChanged.bind(this)
|
||||
this.#targetInfoChanged.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
@ -308,14 +311,11 @@ export class Browser extends EventEmitter {
|
||||
* {@link Puppeteer.connect}.
|
||||
*/
|
||||
process(): ChildProcess | null {
|
||||
return this._process ?? null;
|
||||
return this.#process ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
|
||||
this._isPageTargetCallback =
|
||||
#setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
|
||||
this.#isPageTargetCallback =
|
||||
isPageTargetCallback ||
|
||||
((target: Protocol.Target.TargetInfo): boolean => {
|
||||
return (
|
||||
@ -330,7 +330,7 @@ export class Browser extends EventEmitter {
|
||||
* @internal
|
||||
*/
|
||||
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
|
||||
return this._isPageTargetCallback;
|
||||
return this.#isPageTargetCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -355,7 +355,7 @@ export class Browser extends EventEmitter {
|
||||
): Promise<BrowserContext> {
|
||||
const { proxyServer, proxyBypassList } = options;
|
||||
|
||||
const { browserContextId } = await this._connection.send(
|
||||
const { browserContextId } = await this.#connection.send(
|
||||
'Target.createBrowserContext',
|
||||
{
|
||||
proxyServer,
|
||||
@ -363,11 +363,11 @@ export class Browser extends EventEmitter {
|
||||
}
|
||||
);
|
||||
const context = new BrowserContext(
|
||||
this._connection,
|
||||
this.#connection,
|
||||
this,
|
||||
browserContextId
|
||||
);
|
||||
this._contexts.set(browserContextId, context);
|
||||
this.#contexts.set(browserContextId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
@ -376,64 +376,63 @@ export class Browser extends EventEmitter {
|
||||
* return a single instance of {@link BrowserContext}.
|
||||
*/
|
||||
browserContexts(): BrowserContext[] {
|
||||
return [this._defaultContext, ...Array.from(this._contexts.values())];
|
||||
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default browser context. The default browser context cannot be closed.
|
||||
*/
|
||||
defaultBrowserContext(): BrowserContext {
|
||||
return this._defaultContext;
|
||||
return this.#defaultContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Used by BrowserContext directly so cannot be marked private.
|
||||
*/
|
||||
async _disposeContext(contextId?: string): Promise<void> {
|
||||
if (!contextId) {
|
||||
return;
|
||||
}
|
||||
await this._connection.send('Target.disposeBrowserContext', {
|
||||
await this.#connection.send('Target.disposeBrowserContext', {
|
||||
browserContextId: contextId,
|
||||
});
|
||||
this._contexts.delete(contextId);
|
||||
this.#contexts.delete(contextId);
|
||||
}
|
||||
|
||||
private async _targetCreated(
|
||||
async #targetCreated(
|
||||
event: Protocol.Target.TargetCreatedEvent
|
||||
): Promise<void> {
|
||||
const targetInfo = event.targetInfo;
|
||||
const { browserContextId } = targetInfo;
|
||||
const context =
|
||||
browserContextId && this._contexts.has(browserContextId)
|
||||
? this._contexts.get(browserContextId)
|
||||
: this._defaultContext;
|
||||
browserContextId && this.#contexts.has(browserContextId)
|
||||
? this.#contexts.get(browserContextId)
|
||||
: this.#defaultContext;
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Missing browser context');
|
||||
}
|
||||
|
||||
const shouldAttachToTarget = this._targetFilterCallback(targetInfo);
|
||||
const shouldAttachToTarget = this.#targetFilterCallback(targetInfo);
|
||||
if (!shouldAttachToTarget) {
|
||||
this._ignoredTargets.add(targetInfo.targetId);
|
||||
this.#ignoredTargets.add(targetInfo.targetId);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = new Target(
|
||||
targetInfo,
|
||||
context,
|
||||
() => this._connection.createSession(targetInfo),
|
||||
this._ignoreHTTPSErrors,
|
||||
this._defaultViewport ?? null,
|
||||
this._screenshotTaskQueue,
|
||||
this._isPageTargetCallback
|
||||
() => this.#connection.createSession(targetInfo),
|
||||
this.#ignoreHTTPSErrors,
|
||||
this.#defaultViewport ?? null,
|
||||
this.#screenshotTaskQueue,
|
||||
this.#isPageTargetCallback
|
||||
);
|
||||
assert(
|
||||
!this._targets.has(event.targetInfo.targetId),
|
||||
!this.#targets.has(event.targetInfo.targetId),
|
||||
'Target should not exist before targetCreated'
|
||||
);
|
||||
this._targets.set(event.targetInfo.targetId, target);
|
||||
this.#targets.set(event.targetInfo.targetId, target);
|
||||
|
||||
if (await target._initializedPromise) {
|
||||
this.emit(BrowserEmittedEvents.TargetCreated, target);
|
||||
@ -441,16 +440,16 @@ export class Browser extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async _targetDestroyed(event: { targetId: string }): Promise<void> {
|
||||
if (this._ignoredTargets.has(event.targetId)) return;
|
||||
const target = this._targets.get(event.targetId);
|
||||
async #targetDestroyed(event: { targetId: string }): Promise<void> {
|
||||
if (this.#ignoredTargets.has(event.targetId)) return;
|
||||
const target = this.#targets.get(event.targetId);
|
||||
if (!target) {
|
||||
throw new Error(
|
||||
`Missing target in _targetDestroyed (id = ${event.targetId})`
|
||||
);
|
||||
}
|
||||
target._initializedCallback(false);
|
||||
this._targets.delete(event.targetId);
|
||||
this.#targets.delete(event.targetId);
|
||||
target._closedCallback();
|
||||
if (await target._initializedPromise) {
|
||||
this.emit(BrowserEmittedEvents.TargetDestroyed, target);
|
||||
@ -460,11 +459,9 @@ export class Browser extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private _targetInfoChanged(
|
||||
event: Protocol.Target.TargetInfoChangedEvent
|
||||
): void {
|
||||
if (this._ignoredTargets.has(event.targetInfo.targetId)) return;
|
||||
const target = this._targets.get(event.targetInfo.targetId);
|
||||
#targetInfoChanged(event: Protocol.Target.TargetInfoChangedEvent): void {
|
||||
if (this.#ignoredTargets.has(event.targetInfo.targetId)) return;
|
||||
const target = this.#targets.get(event.targetInfo.targetId);
|
||||
if (!target) {
|
||||
throw new Error(
|
||||
`Missing target in targetInfoChanged (id = ${event.targetInfo.targetId})`
|
||||
@ -499,7 +496,7 @@ export class Browser extends EventEmitter {
|
||||
* | browser endpoint}.
|
||||
*/
|
||||
wsEndpoint(): string {
|
||||
return this._connection.url();
|
||||
return this.#connection.url();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -507,19 +504,18 @@ export class Browser extends EventEmitter {
|
||||
* a default browser context.
|
||||
*/
|
||||
async newPage(): Promise<Page> {
|
||||
return this._defaultContext.newPage();
|
||||
return this.#defaultContext.newPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Used by BrowserContext directly so cannot be marked private.
|
||||
*/
|
||||
async _createPageInContext(contextId?: string): Promise<Page> {
|
||||
const { targetId } = await this._connection.send('Target.createTarget', {
|
||||
const { targetId } = await this.#connection.send('Target.createTarget', {
|
||||
url: 'about:blank',
|
||||
browserContextId: contextId || undefined,
|
||||
});
|
||||
const target = this._targets.get(targetId);
|
||||
const target = this.#targets.get(targetId);
|
||||
if (!target) {
|
||||
throw new Error(`Missing target for page (id = ${targetId})`);
|
||||
}
|
||||
@ -541,7 +537,7 @@ export class Browser extends EventEmitter {
|
||||
* an array with all the targets in all browser contexts.
|
||||
*/
|
||||
targets(): Target[] {
|
||||
return Array.from(this._targets.values()).filter(
|
||||
return Array.from(this.#targets.values()).filter(
|
||||
(target) => target._isInitialized
|
||||
);
|
||||
}
|
||||
@ -628,7 +624,7 @@ export class Browser extends EventEmitter {
|
||||
* The format of browser.version() might change with future releases of Chromium.
|
||||
*/
|
||||
async version(): Promise<string> {
|
||||
const version = await this._getVersion();
|
||||
const version = await this.#getVersion();
|
||||
return version.product;
|
||||
}
|
||||
|
||||
@ -637,7 +633,7 @@ export class Browser extends EventEmitter {
|
||||
* {@link Page.setUserAgent}.
|
||||
*/
|
||||
async userAgent(): Promise<string> {
|
||||
const version = await this._getVersion();
|
||||
const version = await this.#getVersion();
|
||||
return version.userAgent;
|
||||
}
|
||||
|
||||
@ -646,7 +642,7 @@ export class Browser extends EventEmitter {
|
||||
* itself is considered to be disposed and cannot be used anymore.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
await this._closeCallback.call(null);
|
||||
await this.#closeCallback.call(null);
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
@ -656,18 +652,18 @@ export class Browser extends EventEmitter {
|
||||
* cannot be used anymore.
|
||||
*/
|
||||
disconnect(): void {
|
||||
this._connection.dispose();
|
||||
this.#connection.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the browser is connected.
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return !this._connection._closed;
|
||||
return !this.#connection._closed;
|
||||
}
|
||||
|
||||
private _getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
|
||||
return this._connection.send('Browser.getVersion');
|
||||
#getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
|
||||
return this.#connection.send('Browser.getVersion');
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -729,25 +725,25 @@ export const enum BrowserContextEmittedEvents {
|
||||
* @public
|
||||
*/
|
||||
export class BrowserContext extends EventEmitter {
|
||||
private _connection: Connection;
|
||||
private _browser: Browser;
|
||||
private _id?: string;
|
||||
#connection: Connection;
|
||||
#browser: Browser;
|
||||
#id?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(connection: Connection, browser: Browser, contextId?: string) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._browser = browser;
|
||||
this._id = contextId;
|
||||
this.#connection = connection;
|
||||
this.#browser = browser;
|
||||
this.#id = contextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of all active targets inside the browser context.
|
||||
*/
|
||||
targets(): Target[] {
|
||||
return this._browser
|
||||
return this.#browser
|
||||
.targets()
|
||||
.filter((target) => target.browserContext() === this);
|
||||
}
|
||||
@ -773,7 +769,7 @@ export class BrowserContext extends EventEmitter {
|
||||
predicate: (x: Target) => boolean | Promise<boolean>,
|
||||
options: { timeout?: number } = {}
|
||||
): Promise<Target> {
|
||||
return this._browser.waitForTarget(
|
||||
return this.#browser.waitForTarget(
|
||||
(target) => target.browserContext() === this && predicate(target),
|
||||
options
|
||||
);
|
||||
@ -793,7 +789,7 @@ export class BrowserContext extends EventEmitter {
|
||||
(target) =>
|
||||
target.type() === 'page' ||
|
||||
(target.type() === 'other' &&
|
||||
this._browser._getIsPageTargetCallback()?.(
|
||||
this.#browser._getIsPageTargetCallback()?.(
|
||||
target._getTargetInfo()
|
||||
))
|
||||
)
|
||||
@ -810,7 +806,7 @@ export class BrowserContext extends EventEmitter {
|
||||
* The default browser context cannot be closed.
|
||||
*/
|
||||
isIncognito(): boolean {
|
||||
return !!this._id;
|
||||
return !!this.#id;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -835,9 +831,9 @@ export class BrowserContext extends EventEmitter {
|
||||
throw new Error('Unknown permission: ' + permission);
|
||||
return protocolPermission;
|
||||
});
|
||||
await this._connection.send('Browser.grantPermissions', {
|
||||
await this.#connection.send('Browser.grantPermissions', {
|
||||
origin,
|
||||
browserContextId: this._id || undefined,
|
||||
browserContextId: this.#id || undefined,
|
||||
permissions: protocolPermissions,
|
||||
});
|
||||
}
|
||||
@ -854,8 +850,8 @@ export class BrowserContext extends EventEmitter {
|
||||
* ```
|
||||
*/
|
||||
async clearPermissionOverrides(): Promise<void> {
|
||||
await this._connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this._id || undefined,
|
||||
await this.#connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this.#id || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@ -863,14 +859,14 @@ export class BrowserContext extends EventEmitter {
|
||||
* Creates a new page in the browser context.
|
||||
*/
|
||||
newPage(): Promise<Page> {
|
||||
return this._browser._createPageInContext(this._id);
|
||||
return this.#browser._createPageInContext(this.#id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The browser this browser context belongs to.
|
||||
*/
|
||||
browser(): Browser {
|
||||
return this._browser;
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -881,7 +877,7 @@ export class BrowserContext extends EventEmitter {
|
||||
* Only incognito browser contexts can be closed.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
assert(this._id, 'Non-incognito profiles cannot be closed!');
|
||||
await this._browser._disposeContext(this._id);
|
||||
assert(this.#id, 'Non-incognito profiles cannot be closed!');
|
||||
await this.#browser._disposeContext(this.#id);
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export interface BrowserConnectOptions {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isPageTarget?: IsPageTargetCallback;
|
||||
_isPageTarget?: IsPageTargetCallback;
|
||||
}
|
||||
|
||||
const getWebSocketTransportClass = async () => {
|
||||
@ -67,15 +67,16 @@ const getWebSocketTransportClass = async () => {
|
||||
/**
|
||||
* Users should never call this directly; it's called when calling
|
||||
* `puppeteer.connect`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const connectToBrowser = async (
|
||||
export async function _connectToBrowser(
|
||||
options: BrowserConnectOptions & {
|
||||
browserWSEndpoint?: string;
|
||||
browserURL?: string;
|
||||
transport?: ConnectionTransport;
|
||||
}
|
||||
): Promise<Browser> => {
|
||||
): Promise<Browser> {
|
||||
const {
|
||||
browserWSEndpoint,
|
||||
browserURL,
|
||||
@ -84,7 +85,7 @@ export const connectToBrowser = async (
|
||||
transport,
|
||||
slowMo = 0,
|
||||
targetFilter,
|
||||
isPageTarget,
|
||||
_isPageTarget: isPageTarget,
|
||||
} = options;
|
||||
|
||||
assert(
|
||||
@ -112,7 +113,7 @@ export const connectToBrowser = async (
|
||||
const { browserContextIds } = await connection.send(
|
||||
'Target.getBrowserContexts'
|
||||
);
|
||||
return Browser.create(
|
||||
return Browser._create(
|
||||
connection,
|
||||
browserContextIds,
|
||||
ignoreHTTPSErrors,
|
||||
@ -122,7 +123,7 @@ export const connectToBrowser = async (
|
||||
targetFilter,
|
||||
isPageTarget
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
async function getWSEndpoint(browserURL: string): Promise<string> {
|
||||
const endpointURL = new URL('/json/version', browserURL);
|
||||
|
@ -27,27 +27,27 @@ export class BrowserWebSocketTransport implements ConnectionTransport {
|
||||
});
|
||||
}
|
||||
|
||||
private _ws: WebSocket;
|
||||
#ws: WebSocket;
|
||||
onmessage?: (message: string) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(ws: WebSocket) {
|
||||
this._ws = ws;
|
||||
this._ws.addEventListener('message', (event) => {
|
||||
this.#ws = ws;
|
||||
this.#ws.addEventListener('message', (event) => {
|
||||
if (this.onmessage) this.onmessage.call(null, event.data);
|
||||
});
|
||||
this._ws.addEventListener('close', () => {
|
||||
this.#ws.addEventListener('close', () => {
|
||||
if (this.onclose) this.onclose.call(null);
|
||||
});
|
||||
// Silently ignore all errors - we don't know what to do with them.
|
||||
this._ws.addEventListener('error', () => {});
|
||||
this.#ws.addEventListener('error', () => {});
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
this._ws.send(message);
|
||||
this.#ws.send(message);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._ws.close();
|
||||
this.#ws.close();
|
||||
}
|
||||
}
|
||||
|
@ -52,27 +52,33 @@ export const ConnectionEmittedEvents = {
|
||||
* @public
|
||||
*/
|
||||
export class Connection extends EventEmitter {
|
||||
_url: string;
|
||||
_transport: ConnectionTransport;
|
||||
_delay: number;
|
||||
_lastId = 0;
|
||||
_sessions: Map<string, CDPSession> = new Map();
|
||||
_closed = false;
|
||||
|
||||
_callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#url: string;
|
||||
#transport: ConnectionTransport;
|
||||
#delay: number;
|
||||
#lastId = 0;
|
||||
#sessions: Map<string, CDPSession> = new Map();
|
||||
#closed = false;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
|
||||
constructor(url: string, transport: ConnectionTransport, delay = 0) {
|
||||
super();
|
||||
this._url = url;
|
||||
this._delay = delay;
|
||||
this.#url = url;
|
||||
this.#delay = delay;
|
||||
|
||||
this._transport = transport;
|
||||
this._transport.onmessage = this._onMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
this.#transport = transport;
|
||||
this.#transport.onmessage = this.#onMessage.bind(this);
|
||||
this.#transport.onclose = this.#onClose.bind(this);
|
||||
}
|
||||
|
||||
static fromSession(session: CDPSession): Connection | undefined {
|
||||
return session._connection;
|
||||
return session.connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,11 +86,11 @@ export class Connection extends EventEmitter {
|
||||
* @returns The current CDP session if it exists
|
||||
*/
|
||||
session(sessionId: string): CDPSession | null {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
return this.#sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
url(): string {
|
||||
return this._url;
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
send<T extends keyof ProtocolMapping.Commands>(
|
||||
@ -100,7 +106,7 @@ export class Connection extends EventEmitter {
|
||||
const params = paramArgs.length ? paramArgs[0] : undefined;
|
||||
const id = this._rawSend({ method, params });
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {
|
||||
this.#callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
error: new ProtocolError(),
|
||||
@ -109,18 +115,21 @@ export class Connection extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_rawSend(message: Record<string, unknown>): number {
|
||||
const id = ++this._lastId;
|
||||
const id = ++this.#lastId;
|
||||
const stringifiedMessage = JSON.stringify(
|
||||
Object.assign({}, message, { id })
|
||||
);
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this._transport.send(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
return id;
|
||||
}
|
||||
|
||||
async _onMessage(message: string): Promise<void> {
|
||||
if (this._delay) await new Promise((f) => setTimeout(f, this._delay));
|
||||
async #onMessage(message: string): Promise<void> {
|
||||
if (this.#delay) await new Promise((f) => setTimeout(f, this.#delay));
|
||||
debugProtocolReceive(message);
|
||||
const object = JSON.parse(message);
|
||||
if (object.method === 'Target.attachedToTarget') {
|
||||
@ -130,32 +139,32 @@ export class Connection extends EventEmitter {
|
||||
object.params.targetInfo.type,
|
||||
sessionId
|
||||
);
|
||||
this._sessions.set(sessionId, session);
|
||||
this.#sessions.set(sessionId, session);
|
||||
this.emit('sessionattached', session);
|
||||
const parentSession = this._sessions.get(object.sessionId);
|
||||
const parentSession = this.#sessions.get(object.sessionId);
|
||||
if (parentSession) {
|
||||
parentSession.emit('sessionattached', session);
|
||||
}
|
||||
} else if (object.method === 'Target.detachedFromTarget') {
|
||||
const session = this._sessions.get(object.params.sessionId);
|
||||
const session = this.#sessions.get(object.params.sessionId);
|
||||
if (session) {
|
||||
session._onClosed();
|
||||
this._sessions.delete(object.params.sessionId);
|
||||
this.#sessions.delete(object.params.sessionId);
|
||||
this.emit('sessiondetached', session);
|
||||
const parentSession = this._sessions.get(object.sessionId);
|
||||
const parentSession = this.#sessions.get(object.sessionId);
|
||||
if (parentSession) {
|
||||
parentSession.emit('sessiondetached', session);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (object.sessionId) {
|
||||
const session = this._sessions.get(object.sessionId);
|
||||
const session = this.#sessions.get(object.sessionId);
|
||||
if (session) session._onMessage(object);
|
||||
} else if (object.id) {
|
||||
const callback = this._callbacks.get(object.id);
|
||||
const callback = this.#callbacks.get(object.id);
|
||||
// Callbacks could be all rejected if someone has called `.dispose()`.
|
||||
if (callback) {
|
||||
this._callbacks.delete(object.id);
|
||||
this.#callbacks.delete(object.id);
|
||||
if (object.error)
|
||||
callback.reject(
|
||||
createProtocolError(callback.error, callback.method, object)
|
||||
@ -167,27 +176,27 @@ export class Connection extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_onClose(): void {
|
||||
if (this._closed) return;
|
||||
this._closed = true;
|
||||
this._transport.onmessage = undefined;
|
||||
this._transport.onclose = undefined;
|
||||
for (const callback of this._callbacks.values())
|
||||
#onClose(): void {
|
||||
if (this.#closed) return;
|
||||
this.#closed = true;
|
||||
this.#transport.onmessage = undefined;
|
||||
this.#transport.onclose = undefined;
|
||||
for (const callback of this.#callbacks.values())
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.method}): Target closed.`
|
||||
)
|
||||
);
|
||||
this._callbacks.clear();
|
||||
for (const session of this._sessions.values()) session._onClosed();
|
||||
this._sessions.clear();
|
||||
this.#callbacks.clear();
|
||||
for (const session of this.#sessions.values()) session._onClosed();
|
||||
this.#sessions.clear();
|
||||
this.emit(ConnectionEmittedEvents.Disconnected);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onClose();
|
||||
this._transport.close();
|
||||
this.#onClose();
|
||||
this.#transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -201,7 +210,7 @@ export class Connection extends EventEmitter {
|
||||
targetId: targetInfo.targetId,
|
||||
flatten: true,
|
||||
});
|
||||
const session = this._sessions.get(sessionId);
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('CDPSession creation failed.');
|
||||
}
|
||||
@ -255,50 +264,49 @@ export const CDPSessionEmittedEvents = {
|
||||
* @public
|
||||
*/
|
||||
export class CDPSession extends EventEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_connection?: Connection;
|
||||
private _sessionId: string;
|
||||
private _targetType: string;
|
||||
private _callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#sessionId: string;
|
||||
#targetType: string;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#connection?: Connection;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(connection: Connection, targetType: string, sessionId: string) {
|
||||
super();
|
||||
this._connection = connection;
|
||||
this._targetType = targetType;
|
||||
this._sessionId = sessionId;
|
||||
this.#connection = connection;
|
||||
this.#targetType = targetType;
|
||||
this.#sessionId = sessionId;
|
||||
}
|
||||
|
||||
connection(): Connection | undefined {
|
||||
return this._connection;
|
||||
return this.#connection;
|
||||
}
|
||||
|
||||
send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
if (!this._connection)
|
||||
if (!this.#connection)
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`
|
||||
`Protocol error (${method}): Session closed. Most likely the ${
|
||||
this.#targetType
|
||||
} has been closed.`
|
||||
)
|
||||
);
|
||||
|
||||
// See the comment in Connection#send explaining why we do this.
|
||||
const params = paramArgs.length ? paramArgs[0] : undefined;
|
||||
|
||||
const id = this._connection._rawSend({
|
||||
sessionId: this._sessionId,
|
||||
const id = this.#connection._rawSend({
|
||||
sessionId: this.#sessionId,
|
||||
method,
|
||||
params,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, {
|
||||
this.#callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
error: new ProtocolError(),
|
||||
@ -311,9 +319,9 @@ export class CDPSession extends EventEmitter {
|
||||
* @internal
|
||||
*/
|
||||
_onMessage(object: CDPSessionOnMessageObject): void {
|
||||
const callback = object.id ? this._callbacks.get(object.id) : undefined;
|
||||
const callback = object.id ? this.#callbacks.get(object.id) : undefined;
|
||||
if (object.id && callback) {
|
||||
this._callbacks.delete(object.id);
|
||||
this.#callbacks.delete(object.id);
|
||||
if (object.error)
|
||||
callback.reject(
|
||||
createProtocolError(callback.error, callback.method, object)
|
||||
@ -330,12 +338,14 @@ export class CDPSession extends EventEmitter {
|
||||
* won't emit any events and can't be used to send messages.
|
||||
*/
|
||||
async detach(): Promise<void> {
|
||||
if (!this._connection)
|
||||
if (!this.#connection)
|
||||
throw new Error(
|
||||
`Session already detached. Most likely the ${this._targetType} has been closed.`
|
||||
`Session already detached. Most likely the ${
|
||||
this.#targetType
|
||||
} has been closed.`
|
||||
);
|
||||
await this._connection.send('Target.detachFromTarget', {
|
||||
sessionId: this._sessionId,
|
||||
await this.#connection.send('Target.detachFromTarget', {
|
||||
sessionId: this.#sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -343,23 +353,23 @@ export class CDPSession extends EventEmitter {
|
||||
* @internal
|
||||
*/
|
||||
_onClosed(): void {
|
||||
for (const callback of this._callbacks.values())
|
||||
for (const callback of this.#callbacks.values())
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.method}): Target closed.`
|
||||
)
|
||||
);
|
||||
this._callbacks.clear();
|
||||
this._connection = undefined;
|
||||
this.#callbacks.clear();
|
||||
this.#connection = undefined;
|
||||
this.emit(CDPSessionEmittedEvents.Disconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Returns the session's id.
|
||||
*/
|
||||
id(): string {
|
||||
return this._sessionId;
|
||||
return this.#sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,10 +66,10 @@ export type ConsoleMessageType =
|
||||
* @public
|
||||
*/
|
||||
export class ConsoleMessage {
|
||||
private _type: ConsoleMessageType;
|
||||
private _text: string;
|
||||
private _args: JSHandle[];
|
||||
private _stackTraceLocations: ConsoleMessageLocation[];
|
||||
#type: ConsoleMessageType;
|
||||
#text: string;
|
||||
#args: JSHandle[];
|
||||
#stackTraceLocations: ConsoleMessageLocation[];
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -80,44 +80,44 @@ export class ConsoleMessage {
|
||||
args: JSHandle[],
|
||||
stackTraceLocations: ConsoleMessageLocation[]
|
||||
) {
|
||||
this._type = type;
|
||||
this._text = text;
|
||||
this._args = args;
|
||||
this._stackTraceLocations = stackTraceLocations;
|
||||
this.#type = type;
|
||||
this.#text = text;
|
||||
this.#args = args;
|
||||
this.#stackTraceLocations = stackTraceLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The type of the console message.
|
||||
*/
|
||||
type(): ConsoleMessageType {
|
||||
return this._type;
|
||||
return this.#type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The text of the console message.
|
||||
*/
|
||||
text(): string {
|
||||
return this._text;
|
||||
return this.#text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns An array of arguments passed to the console.
|
||||
*/
|
||||
args(): JSHandle[] {
|
||||
return this._args;
|
||||
return this.#args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The location of the console message.
|
||||
*/
|
||||
location(): ConsoleMessageLocation {
|
||||
return this._stackTraceLocations[0] ?? {};
|
||||
return this.#stackTraceLocations[0] ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The array of locations on the stack of the console message.
|
||||
*/
|
||||
stackTrace(): ConsoleMessageLocation[] {
|
||||
return this._stackTraceLocations;
|
||||
return this.#stackTraceLocations;
|
||||
}
|
||||
}
|
||||
|
@ -123,18 +123,12 @@ export interface CSSCoverageOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Coverage {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_jsCoverage: JSCoverage;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_cssCoverage: CSSCoverage;
|
||||
#jsCoverage: JSCoverage;
|
||||
#cssCoverage: CSSCoverage;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this._jsCoverage = new JSCoverage(client);
|
||||
this._cssCoverage = new CSSCoverage(client);
|
||||
this.#jsCoverage = new JSCoverage(client);
|
||||
this.#cssCoverage = new CSSCoverage(client);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +143,7 @@ export class Coverage {
|
||||
* scripts will have `pptr://__puppeteer_evaluation_script__` as their URL.
|
||||
*/
|
||||
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
|
||||
return await this._jsCoverage.start(options);
|
||||
return await this.#jsCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,7 +155,7 @@ export class Coverage {
|
||||
* However, scripts with sourceURLs are reported.
|
||||
*/
|
||||
async stopJSCoverage(): Promise<JSCoverageEntry[]> {
|
||||
return await this._jsCoverage.stop();
|
||||
return await this.#jsCoverage.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,7 +164,7 @@ export class Coverage {
|
||||
* @returns Promise that resolves when coverage is started.
|
||||
*/
|
||||
async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> {
|
||||
return await this._cssCoverage.start(options);
|
||||
return await this.#cssCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -181,7 +175,7 @@ export class Coverage {
|
||||
* without sourceURLs.
|
||||
*/
|
||||
async stopCSSCoverage(): Promise<CoverageEntry[]> {
|
||||
return await this._cssCoverage.stop();
|
||||
return await this.#cssCoverage.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,17 +183,17 @@ export class Coverage {
|
||||
* @public
|
||||
*/
|
||||
export class JSCoverage {
|
||||
_client: CDPSession;
|
||||
_enabled = false;
|
||||
_scriptURLs = new Map<string, string>();
|
||||
_scriptSources = new Map<string, string>();
|
||||
_eventListeners: PuppeteerEventListener[] = [];
|
||||
_resetOnNavigation = false;
|
||||
_reportAnonymousScripts = false;
|
||||
_includeRawScriptCoverage = false;
|
||||
#client: CDPSession;
|
||||
#enabled = false;
|
||||
#scriptURLs = new Map<string, string>();
|
||||
#scriptSources = new Map<string, string>();
|
||||
#eventListeners: PuppeteerEventListener[] = [];
|
||||
#resetOnNavigation = false;
|
||||
#reportAnonymousScripts = false;
|
||||
#includeRawScriptCoverage = false;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(
|
||||
@ -209,60 +203,60 @@ export class JSCoverage {
|
||||
includeRawScriptCoverage?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
assert(!this._enabled, 'JSCoverage is already enabled');
|
||||
assert(!this.#enabled, 'JSCoverage is already enabled');
|
||||
const {
|
||||
resetOnNavigation = true,
|
||||
reportAnonymousScripts = false,
|
||||
includeRawScriptCoverage = false,
|
||||
} = options;
|
||||
this._resetOnNavigation = resetOnNavigation;
|
||||
this._reportAnonymousScripts = reportAnonymousScripts;
|
||||
this._includeRawScriptCoverage = includeRawScriptCoverage;
|
||||
this._enabled = true;
|
||||
this._scriptURLs.clear();
|
||||
this._scriptSources.clear();
|
||||
this._eventListeners = [
|
||||
this.#resetOnNavigation = resetOnNavigation;
|
||||
this.#reportAnonymousScripts = reportAnonymousScripts;
|
||||
this.#includeRawScriptCoverage = includeRawScriptCoverage;
|
||||
this.#enabled = true;
|
||||
this.#scriptURLs.clear();
|
||||
this.#scriptSources.clear();
|
||||
this.#eventListeners = [
|
||||
helper.addEventListener(
|
||||
this._client,
|
||||
this.#client,
|
||||
'Debugger.scriptParsed',
|
||||
this._onScriptParsed.bind(this)
|
||||
this.#onScriptParsed.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._client,
|
||||
this.#client,
|
||||
'Runtime.executionContextsCleared',
|
||||
this._onExecutionContextsCleared.bind(this)
|
||||
this.#onExecutionContextsCleared.bind(this)
|
||||
),
|
||||
];
|
||||
await Promise.all([
|
||||
this._client.send('Profiler.enable'),
|
||||
this._client.send('Profiler.startPreciseCoverage', {
|
||||
callCount: this._includeRawScriptCoverage,
|
||||
this.#client.send('Profiler.enable'),
|
||||
this.#client.send('Profiler.startPreciseCoverage', {
|
||||
callCount: this.#includeRawScriptCoverage,
|
||||
detailed: true,
|
||||
}),
|
||||
this._client.send('Debugger.enable'),
|
||||
this._client.send('Debugger.setSkipAllPauses', { skip: true }),
|
||||
this.#client.send('Debugger.enable'),
|
||||
this.#client.send('Debugger.setSkipAllPauses', { skip: true }),
|
||||
]);
|
||||
}
|
||||
|
||||
_onExecutionContextsCleared(): void {
|
||||
if (!this._resetOnNavigation) return;
|
||||
this._scriptURLs.clear();
|
||||
this._scriptSources.clear();
|
||||
#onExecutionContextsCleared(): void {
|
||||
if (!this.#resetOnNavigation) return;
|
||||
this.#scriptURLs.clear();
|
||||
this.#scriptSources.clear();
|
||||
}
|
||||
|
||||
async _onScriptParsed(
|
||||
async #onScriptParsed(
|
||||
event: Protocol.Debugger.ScriptParsedEvent
|
||||
): Promise<void> {
|
||||
// Ignore puppeteer-injected scripts
|
||||
if (event.url === EVALUATION_SCRIPT_URL) return;
|
||||
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
||||
if (!event.url && !this._reportAnonymousScripts) return;
|
||||
if (!event.url && !this.#reportAnonymousScripts) return;
|
||||
try {
|
||||
const response = await this._client.send('Debugger.getScriptSource', {
|
||||
const response = await this.#client.send('Debugger.getScriptSource', {
|
||||
scriptId: event.scriptId,
|
||||
});
|
||||
this._scriptURLs.set(event.scriptId, event.url);
|
||||
this._scriptSources.set(event.scriptId, response.scriptSource);
|
||||
this.#scriptURLs.set(event.scriptId, event.url);
|
||||
this.#scriptSources.set(event.scriptId, response.scriptSource);
|
||||
} catch (error) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(error);
|
||||
@ -270,31 +264,31 @@ export class JSCoverage {
|
||||
}
|
||||
|
||||
async stop(): Promise<JSCoverageEntry[]> {
|
||||
assert(this._enabled, 'JSCoverage is not enabled');
|
||||
this._enabled = false;
|
||||
assert(this.#enabled, 'JSCoverage is not enabled');
|
||||
this.#enabled = false;
|
||||
|
||||
const result = await Promise.all([
|
||||
this._client.send('Profiler.takePreciseCoverage'),
|
||||
this._client.send('Profiler.stopPreciseCoverage'),
|
||||
this._client.send('Profiler.disable'),
|
||||
this._client.send('Debugger.disable'),
|
||||
this.#client.send('Profiler.takePreciseCoverage'),
|
||||
this.#client.send('Profiler.stopPreciseCoverage'),
|
||||
this.#client.send('Profiler.disable'),
|
||||
this.#client.send('Debugger.disable'),
|
||||
]);
|
||||
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
helper.removeEventListeners(this.#eventListeners);
|
||||
|
||||
const coverage = [];
|
||||
const profileResponse = result[0];
|
||||
|
||||
for (const entry of profileResponse.result) {
|
||||
let url = this._scriptURLs.get(entry.scriptId);
|
||||
if (!url && this._reportAnonymousScripts)
|
||||
let url = this.#scriptURLs.get(entry.scriptId);
|
||||
if (!url && this.#reportAnonymousScripts)
|
||||
url = 'debugger://VM' + entry.scriptId;
|
||||
const text = this._scriptSources.get(entry.scriptId);
|
||||
const text = this.#scriptSources.get(entry.scriptId);
|
||||
if (text === undefined || url === undefined) continue;
|
||||
const flattenRanges = [];
|
||||
for (const func of entry.functions) flattenRanges.push(...func.ranges);
|
||||
const ranges = convertToDisjointRanges(flattenRanges);
|
||||
if (!this._includeRawScriptCoverage) {
|
||||
if (!this.#includeRawScriptCoverage) {
|
||||
coverage.push({ url, ranges, text });
|
||||
} else {
|
||||
coverage.push({ url, ranges, text, rawScriptCoverage: entry });
|
||||
@ -308,60 +302,59 @@ export class JSCoverage {
|
||||
* @public
|
||||
*/
|
||||
export class CSSCoverage {
|
||||
_client: CDPSession;
|
||||
_enabled = false;
|
||||
_stylesheetURLs = new Map<string, string>();
|
||||
_stylesheetSources = new Map<string, string>();
|
||||
_eventListeners: PuppeteerEventListener[] = [];
|
||||
_resetOnNavigation = false;
|
||||
_reportAnonymousScripts = false;
|
||||
#client: CDPSession;
|
||||
#enabled = false;
|
||||
#stylesheetURLs = new Map<string, string>();
|
||||
#stylesheetSources = new Map<string, string>();
|
||||
#eventListeners: PuppeteerEventListener[] = [];
|
||||
#resetOnNavigation = false;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(options: { resetOnNavigation?: boolean } = {}): Promise<void> {
|
||||
assert(!this._enabled, 'CSSCoverage is already enabled');
|
||||
assert(!this.#enabled, 'CSSCoverage is already enabled');
|
||||
const { resetOnNavigation = true } = options;
|
||||
this._resetOnNavigation = resetOnNavigation;
|
||||
this._enabled = true;
|
||||
this._stylesheetURLs.clear();
|
||||
this._stylesheetSources.clear();
|
||||
this._eventListeners = [
|
||||
this.#resetOnNavigation = resetOnNavigation;
|
||||
this.#enabled = true;
|
||||
this.#stylesheetURLs.clear();
|
||||
this.#stylesheetSources.clear();
|
||||
this.#eventListeners = [
|
||||
helper.addEventListener(
|
||||
this._client,
|
||||
this.#client,
|
||||
'CSS.styleSheetAdded',
|
||||
this._onStyleSheet.bind(this)
|
||||
this.#onStyleSheet.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._client,
|
||||
this.#client,
|
||||
'Runtime.executionContextsCleared',
|
||||
this._onExecutionContextsCleared.bind(this)
|
||||
this.#onExecutionContextsCleared.bind(this)
|
||||
),
|
||||
];
|
||||
await Promise.all([
|
||||
this._client.send('DOM.enable'),
|
||||
this._client.send('CSS.enable'),
|
||||
this._client.send('CSS.startRuleUsageTracking'),
|
||||
this.#client.send('DOM.enable'),
|
||||
this.#client.send('CSS.enable'),
|
||||
this.#client.send('CSS.startRuleUsageTracking'),
|
||||
]);
|
||||
}
|
||||
|
||||
_onExecutionContextsCleared(): void {
|
||||
if (!this._resetOnNavigation) return;
|
||||
this._stylesheetURLs.clear();
|
||||
this._stylesheetSources.clear();
|
||||
#onExecutionContextsCleared(): void {
|
||||
if (!this.#resetOnNavigation) return;
|
||||
this.#stylesheetURLs.clear();
|
||||
this.#stylesheetSources.clear();
|
||||
}
|
||||
|
||||
async _onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
|
||||
async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
|
||||
const header = event.header;
|
||||
// Ignore anonymous scripts
|
||||
if (!header.sourceURL) return;
|
||||
try {
|
||||
const response = await this._client.send('CSS.getStyleSheetText', {
|
||||
const response = await this.#client.send('CSS.getStyleSheetText', {
|
||||
styleSheetId: header.styleSheetId,
|
||||
});
|
||||
this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
|
||||
this._stylesheetSources.set(header.styleSheetId, response.text);
|
||||
this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
|
||||
this.#stylesheetSources.set(header.styleSheetId, response.text);
|
||||
} catch (error) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(error);
|
||||
@ -369,16 +362,16 @@ export class CSSCoverage {
|
||||
}
|
||||
|
||||
async stop(): Promise<CoverageEntry[]> {
|
||||
assert(this._enabled, 'CSSCoverage is not enabled');
|
||||
this._enabled = false;
|
||||
const ruleTrackingResponse = await this._client.send(
|
||||
assert(this.#enabled, 'CSSCoverage is not enabled');
|
||||
this.#enabled = false;
|
||||
const ruleTrackingResponse = await this.#client.send(
|
||||
'CSS.stopRuleUsageTracking'
|
||||
);
|
||||
await Promise.all([
|
||||
this._client.send('CSS.disable'),
|
||||
this._client.send('DOM.disable'),
|
||||
this.#client.send('CSS.disable'),
|
||||
this.#client.send('DOM.disable'),
|
||||
]);
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
helper.removeEventListeners(this.#eventListeners);
|
||||
|
||||
// aggregate by styleSheetId
|
||||
const styleSheetIdToCoverage = new Map();
|
||||
@ -396,10 +389,10 @@ export class CSSCoverage {
|
||||
}
|
||||
|
||||
const coverage: CoverageEntry[] = [];
|
||||
for (const styleSheetId of this._stylesheetURLs.keys()) {
|
||||
const url = this._stylesheetURLs.get(styleSheetId);
|
||||
for (const styleSheetId of this.#stylesheetURLs.keys()) {
|
||||
const url = this.#stylesheetURLs.get(styleSheetId);
|
||||
assert(url);
|
||||
const text = this._stylesheetSources.get(styleSheetId);
|
||||
const text = this.#stylesheetSources.get(styleSheetId);
|
||||
assert(text);
|
||||
const ranges = convertToDisjointRanges(
|
||||
styleSheetIdToCoverage.get(styleSheetId) || []
|
||||
|
@ -35,7 +35,7 @@ import {
|
||||
LifecycleWatcher,
|
||||
PuppeteerLifeCycleEvent,
|
||||
} from './LifecycleWatcher.js';
|
||||
import { getQueryHandlerAndSelector } from './QueryHandler.js';
|
||||
import { _getQueryHandlerAndSelector } from './QueryHandler.js';
|
||||
import { TimeoutSettings } from './TimeoutSettings.js';
|
||||
|
||||
// predicateQueryHandler and checkWaitForOptions are declared here so that
|
||||
@ -72,30 +72,37 @@ export interface PageBinding {
|
||||
* @internal
|
||||
*/
|
||||
export class DOMWorld {
|
||||
private _frameManager: FrameManager;
|
||||
private _client: CDPSession;
|
||||
private _frame: Frame;
|
||||
private _timeoutSettings: TimeoutSettings;
|
||||
private _documentPromise: Promise<ElementHandle> | null = null;
|
||||
private _contextPromise: Promise<ExecutionContext> | null = null;
|
||||
#frameManager: FrameManager;
|
||||
#client: CDPSession;
|
||||
#frame: Frame;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#documentPromise: Promise<ElementHandle> | null = null;
|
||||
#contextPromise: Promise<ExecutionContext> | null = null;
|
||||
#contextResolveCallback: ((x: ExecutionContext) => void) | null = null;
|
||||
#detached = false;
|
||||
|
||||
private _contextResolveCallback: ((x: ExecutionContext) => void) | null =
|
||||
null;
|
||||
|
||||
private _detached = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_waitTasks = new Set<WaitTask>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Contains mapping from functions that should be bound to Puppeteer functions.
|
||||
*/
|
||||
_boundFunctions = new Map<string, Function>();
|
||||
// Set of bindings that have been registered in the current context.
|
||||
private _ctxBindings = new Set<string>();
|
||||
private static bindingIdentifier = (name: string, contextId: number) =>
|
||||
#ctxBindings = new Set<string>();
|
||||
|
||||
// Contains mapping from functions that should be bound to Puppeteer functions.
|
||||
#boundFunctions = new Map<string, Function>();
|
||||
#waitTasks = new Set<WaitTask>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _waitTasks(): Set<WaitTask> {
|
||||
return this.#waitTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _boundFunctions(): Map<string, Function> {
|
||||
return this.#boundFunctions;
|
||||
}
|
||||
|
||||
static #bindingIdentifier = (name: string, contextId: number) =>
|
||||
`${name}_${contextId}`;
|
||||
|
||||
constructor(
|
||||
@ -106,44 +113,52 @@ export class DOMWorld {
|
||||
) {
|
||||
// Keep own reference to client because it might differ from the FrameManager's
|
||||
// client for OOP iframes.
|
||||
this._client = client;
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this.#client = client;
|
||||
this.#frameManager = frameManager;
|
||||
this.#frame = frame;
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this._setContext(null);
|
||||
this._onBindingCalled = this._onBindingCalled.bind(this);
|
||||
this._client.on('Runtime.bindingCalled', this._onBindingCalled);
|
||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
}
|
||||
|
||||
frame(): Frame {
|
||||
return this._frame;
|
||||
return this.#frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _setContext(context: ExecutionContext | null): Promise<void> {
|
||||
if (context) {
|
||||
assert(
|
||||
this._contextResolveCallback,
|
||||
this.#contextResolveCallback,
|
||||
'Execution Context has already been set.'
|
||||
);
|
||||
this._ctxBindings.clear();
|
||||
this._contextResolveCallback?.call(null, context);
|
||||
this._contextResolveCallback = null;
|
||||
this.#ctxBindings.clear();
|
||||
this.#contextResolveCallback?.call(null, context);
|
||||
this.#contextResolveCallback = null;
|
||||
for (const waitTask of this._waitTasks) waitTask.rerun();
|
||||
} else {
|
||||
this._documentPromise = null;
|
||||
this._contextPromise = new Promise((fulfill) => {
|
||||
this._contextResolveCallback = fulfill;
|
||||
this.#documentPromise = null;
|
||||
this.#contextPromise = new Promise((fulfill) => {
|
||||
this.#contextResolveCallback = fulfill;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_hasContext(): boolean {
|
||||
return !this._contextResolveCallback;
|
||||
return !this.#contextResolveCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_detach(): void {
|
||||
this._detached = true;
|
||||
this._client.off('Runtime.bindingCalled', this._onBindingCalled);
|
||||
this.#detached = true;
|
||||
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
for (const waitTask of this._waitTasks)
|
||||
waitTask.terminate(
|
||||
new Error('waitForFunction failed: frame got detached.')
|
||||
@ -151,13 +166,13 @@ export class DOMWorld {
|
||||
}
|
||||
|
||||
executionContext(): Promise<ExecutionContext> {
|
||||
if (this._detached)
|
||||
if (this.#detached)
|
||||
throw new Error(
|
||||
`Execution context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`
|
||||
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`
|
||||
);
|
||||
if (this._contextPromise === null)
|
||||
if (this.#contextPromise === null)
|
||||
throw new Error(`Execution content promise is missing`);
|
||||
return this._contextPromise;
|
||||
return this.#contextPromise;
|
||||
}
|
||||
|
||||
async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
|
||||
@ -187,9 +202,12 @@ export class DOMWorld {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _document(): Promise<ElementHandle> {
|
||||
if (this._documentPromise) return this._documentPromise;
|
||||
this._documentPromise = this.executionContext().then(async (context) => {
|
||||
if (this.#documentPromise) return this.#documentPromise;
|
||||
this.#documentPromise = this.executionContext().then(async (context) => {
|
||||
const document = await context.evaluateHandle('document');
|
||||
const element = document.asElement();
|
||||
if (element === null) {
|
||||
@ -197,7 +215,7 @@ export class DOMWorld {
|
||||
}
|
||||
return element;
|
||||
});
|
||||
return this._documentPromise;
|
||||
return this.#documentPromise;
|
||||
}
|
||||
|
||||
async $x(expression: string): Promise<ElementHandle[]> {
|
||||
@ -263,7 +281,7 @@ export class DOMWorld {
|
||||
): Promise<void> {
|
||||
const {
|
||||
waitUntil = ['load'],
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
timeout = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
||||
// lifecycle event. @see https://crrev.com/608658
|
||||
@ -273,8 +291,8 @@ export class DOMWorld {
|
||||
document.close();
|
||||
}, html);
|
||||
const watcher = new LifecycleWatcher(
|
||||
this._frameManager,
|
||||
this._frame,
|
||||
this.#frameManager,
|
||||
this.#frame,
|
||||
waitUntil,
|
||||
timeout
|
||||
);
|
||||
@ -560,33 +578,34 @@ export class DOMWorld {
|
||||
options: WaitForSelectorOptions
|
||||
): Promise<ElementHandle | null> {
|
||||
const { updatedSelector, queryHandler } =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
_getQueryHandlerAndSelector(selector);
|
||||
assert(queryHandler.waitFor, 'Query handler does not support waiting');
|
||||
return queryHandler.waitFor(this, updatedSelector, options);
|
||||
}
|
||||
|
||||
// If multiple waitFor are set up asynchronously, we need to wait for the
|
||||
// first one to set up the binding in the page before running the others.
|
||||
private _settingUpBinding: Promise<void> | null = null;
|
||||
#settingUpBinding: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async addBindingToContext(
|
||||
async _addBindingToContext(
|
||||
context: ExecutionContext,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
// Previous operation added the binding so we are done.
|
||||
if (
|
||||
this._ctxBindings.has(
|
||||
DOMWorld.bindingIdentifier(name, context._contextId)
|
||||
this.#ctxBindings.has(
|
||||
DOMWorld.#bindingIdentifier(name, context._contextId)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Wait for other operation to finish
|
||||
if (this._settingUpBinding) {
|
||||
await this._settingUpBinding;
|
||||
return this.addBindingToContext(context, name);
|
||||
if (this.#settingUpBinding) {
|
||||
await this.#settingUpBinding;
|
||||
return this._addBindingToContext(context, name);
|
||||
}
|
||||
|
||||
const bind = async (name: string) => {
|
||||
@ -617,19 +636,19 @@ export class DOMWorld {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._ctxBindings.add(
|
||||
DOMWorld.bindingIdentifier(name, context._contextId)
|
||||
this.#ctxBindings.add(
|
||||
DOMWorld.#bindingIdentifier(name, context._contextId)
|
||||
);
|
||||
};
|
||||
|
||||
this._settingUpBinding = bind(name);
|
||||
await this._settingUpBinding;
|
||||
this._settingUpBinding = null;
|
||||
this.#settingUpBinding = bind(name);
|
||||
await this.#settingUpBinding;
|
||||
this.#settingUpBinding = null;
|
||||
}
|
||||
|
||||
private async _onBindingCalled(
|
||||
#onBindingCalled = async (
|
||||
event: Protocol.Runtime.BindingCalledEvent
|
||||
): Promise<void> {
|
||||
): Promise<void> => {
|
||||
let payload: { type: string; name: string; seq: number; args: unknown[] };
|
||||
if (!this._hasContext()) return;
|
||||
const context = await this.executionContext();
|
||||
@ -643,8 +662,8 @@ export class DOMWorld {
|
||||
const { type, name, seq, args } = payload;
|
||||
if (
|
||||
type !== 'internal' ||
|
||||
!this._ctxBindings.has(
|
||||
DOMWorld.bindingIdentifier(name, context._contextId)
|
||||
!this.#ctxBindings.has(
|
||||
DOMWorld.#bindingIdentifier(name, context._contextId)
|
||||
)
|
||||
)
|
||||
return;
|
||||
@ -673,12 +692,12 @@ export class DOMWorld {
|
||||
// @ts-ignore Code is evaluated in a different context.
|
||||
globalThis[name].callbacks.delete(seq);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async waitForSelectorInPage(
|
||||
async _waitForSelectorInPage(
|
||||
queryOne: Function,
|
||||
selector: string,
|
||||
options: WaitForSelectorOptions,
|
||||
@ -687,7 +706,7 @@ export class DOMWorld {
|
||||
const {
|
||||
visible: waitForVisible = false,
|
||||
hidden: waitForHidden = false,
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
} = options;
|
||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||
const title = `selector \`${selector}\`${
|
||||
@ -732,7 +751,7 @@ export class DOMWorld {
|
||||
const {
|
||||
visible: waitForVisible = false,
|
||||
hidden: waitForHidden = false,
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
} = options;
|
||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||
const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`;
|
||||
@ -776,7 +795,7 @@ export class DOMWorld {
|
||||
options: { polling?: string | number; timeout?: number } = {},
|
||||
...args: SerializableOrJSHandle[]
|
||||
): Promise<JSHandle> {
|
||||
const { polling = 'raf', timeout = this._timeoutSettings.timeout() } =
|
||||
const { polling = 'raf', timeout = this.#timeoutSettings.timeout() } =
|
||||
options;
|
||||
const waitTaskOptions: WaitTaskOptions = {
|
||||
domWorld: this,
|
||||
@ -817,20 +836,21 @@ const noop = (): void => {};
|
||||
* @internal
|
||||
*/
|
||||
export class WaitTask {
|
||||
_domWorld: DOMWorld;
|
||||
_polling: string | number;
|
||||
_timeout: number;
|
||||
_predicateBody: string;
|
||||
_predicateAcceptsContextElement: boolean;
|
||||
_args: SerializableOrJSHandle[];
|
||||
_binding?: PageBinding;
|
||||
_runCount = 0;
|
||||
#domWorld: DOMWorld;
|
||||
#polling: string | number;
|
||||
#timeout: number;
|
||||
#predicateBody: string;
|
||||
#predicateAcceptsContextElement: boolean;
|
||||
#args: SerializableOrJSHandle[];
|
||||
#binding?: PageBinding;
|
||||
#runCount = 0;
|
||||
#resolve: (x: JSHandle) => void = noop;
|
||||
#reject: (x: Error) => void = noop;
|
||||
#timeoutTimer?: NodeJS.Timeout;
|
||||
#terminated = false;
|
||||
#root: ElementHandle | null = null;
|
||||
|
||||
promise: Promise<JSHandle>;
|
||||
_resolve: (x: JSHandle) => void = noop;
|
||||
_reject: (x: Error) => void = noop;
|
||||
_timeoutTimer?: NodeJS.Timeout;
|
||||
_terminated = false;
|
||||
_root: ElementHandle | null = null;
|
||||
|
||||
constructor(options: WaitTaskOptions) {
|
||||
if (helper.isString(options.polling))
|
||||
@ -850,26 +870,26 @@ export class WaitTask {
|
||||
return `return (${predicateBody})(...args);`;
|
||||
}
|
||||
|
||||
this._domWorld = options.domWorld;
|
||||
this._polling = options.polling;
|
||||
this._timeout = options.timeout;
|
||||
this._root = options.root || null;
|
||||
this._predicateBody = getPredicateBody(options.predicateBody);
|
||||
this._predicateAcceptsContextElement =
|
||||
this.#domWorld = options.domWorld;
|
||||
this.#polling = options.polling;
|
||||
this.#timeout = options.timeout;
|
||||
this.#root = options.root || null;
|
||||
this.#predicateBody = getPredicateBody(options.predicateBody);
|
||||
this.#predicateAcceptsContextElement =
|
||||
options.predicateAcceptsContextElement;
|
||||
this._args = options.args;
|
||||
this._binding = options.binding;
|
||||
this._runCount = 0;
|
||||
this._domWorld._waitTasks.add(this);
|
||||
if (this._binding) {
|
||||
this._domWorld._boundFunctions.set(
|
||||
this._binding.name,
|
||||
this._binding.pptrFunction
|
||||
this.#args = options.args;
|
||||
this.#binding = options.binding;
|
||||
this.#runCount = 0;
|
||||
this.#domWorld._waitTasks.add(this);
|
||||
if (this.#binding) {
|
||||
this.#domWorld._boundFunctions.set(
|
||||
this.#binding.name,
|
||||
this.#binding.pptrFunction
|
||||
);
|
||||
}
|
||||
this.promise = new Promise<JSHandle>((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
this.#resolve = resolve;
|
||||
this.#reject = reject;
|
||||
});
|
||||
// Since page navigation requires us to re-install the pageScript, we should track
|
||||
// timeout on our end.
|
||||
@ -877,7 +897,7 @@ export class WaitTask {
|
||||
const timeoutError = new TimeoutError(
|
||||
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
|
||||
);
|
||||
this._timeoutTimer = setTimeout(
|
||||
this.#timeoutTimer = setTimeout(
|
||||
() => this.terminate(timeoutError),
|
||||
options.timeout
|
||||
);
|
||||
@ -886,36 +906,36 @@ export class WaitTask {
|
||||
}
|
||||
|
||||
terminate(error: Error): void {
|
||||
this._terminated = true;
|
||||
this._reject(error);
|
||||
this._cleanup();
|
||||
this.#terminated = true;
|
||||
this.#reject(error);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
async rerun(): Promise<void> {
|
||||
const runCount = ++this._runCount;
|
||||
const runCount = ++this.#runCount;
|
||||
let success: JSHandle | null = null;
|
||||
let error: Error | null = null;
|
||||
const context = await this._domWorld.executionContext();
|
||||
if (this._terminated || runCount !== this._runCount) return;
|
||||
if (this._binding) {
|
||||
await this._domWorld.addBindingToContext(context, this._binding.name);
|
||||
const context = await this.#domWorld.executionContext();
|
||||
if (this.#terminated || runCount !== this.#runCount) return;
|
||||
if (this.#binding) {
|
||||
await this.#domWorld._addBindingToContext(context, this.#binding.name);
|
||||
}
|
||||
if (this._terminated || runCount !== this._runCount) return;
|
||||
if (this.#terminated || runCount !== this.#runCount) return;
|
||||
try {
|
||||
success = await context.evaluateHandle(
|
||||
waitForPredicatePageFunction,
|
||||
this._root || null,
|
||||
this._predicateBody,
|
||||
this._predicateAcceptsContextElement,
|
||||
this._polling,
|
||||
this._timeout,
|
||||
...this._args
|
||||
this.#root || null,
|
||||
this.#predicateBody,
|
||||
this.#predicateAcceptsContextElement,
|
||||
this.#polling,
|
||||
this.#timeout,
|
||||
...this.#args
|
||||
);
|
||||
} catch (error_) {
|
||||
error = error_ as Error;
|
||||
}
|
||||
|
||||
if (this._terminated || runCount !== this._runCount) {
|
||||
if (this.#terminated || runCount !== this.#runCount) {
|
||||
if (success) await success.dispose();
|
||||
return;
|
||||
}
|
||||
@ -925,7 +945,7 @@ export class WaitTask {
|
||||
// throw an error - ignore this predicate run altogether.
|
||||
if (
|
||||
!error &&
|
||||
(await this._domWorld.evaluate((s) => !s, success).catch(() => true))
|
||||
(await this.#domWorld.evaluate((s) => !s, success).catch(() => true))
|
||||
) {
|
||||
if (!success)
|
||||
throw new Error('Assertion: result handle is not available');
|
||||
@ -959,18 +979,18 @@ export class WaitTask {
|
||||
if (error.message.includes('Cannot find context with specified id'))
|
||||
return;
|
||||
|
||||
this._reject(error);
|
||||
this.#reject(error);
|
||||
} else {
|
||||
if (!success)
|
||||
throw new Error('Assertion: result handle is not available');
|
||||
this._resolve(success);
|
||||
this.#resolve(success);
|
||||
}
|
||||
this._cleanup();
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
_cleanup(): void {
|
||||
this._timeoutTimer !== undefined && clearTimeout(this._timeoutTimer);
|
||||
this._domWorld._waitTasks.delete(this);
|
||||
#cleanup(): void {
|
||||
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
|
||||
this.#domWorld._waitTasks.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1537,6 +1537,8 @@ export type DevicesMap = {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const devicesMap: DevicesMap = {};
|
||||
export const _devicesMap: DevicesMap = {};
|
||||
|
||||
for (const device of devices) devicesMap[device.name] = device;
|
||||
for (const device of devices) {
|
||||
_devicesMap[device.name] = device;
|
||||
}
|
||||
|
@ -41,11 +41,11 @@ import { Protocol } from 'devtools-protocol';
|
||||
* @public
|
||||
*/
|
||||
export class Dialog {
|
||||
private _client: CDPSession;
|
||||
private _type: Protocol.Page.DialogType;
|
||||
private _message: string;
|
||||
private _defaultValue: string;
|
||||
private _handled = false;
|
||||
#client: CDPSession;
|
||||
#type: Protocol.Page.DialogType;
|
||||
#message: string;
|
||||
#defaultValue: string;
|
||||
#handled = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -56,24 +56,24 @@ export class Dialog {
|
||||
message: string,
|
||||
defaultValue = ''
|
||||
) {
|
||||
this._client = client;
|
||||
this._type = type;
|
||||
this._message = message;
|
||||
this._defaultValue = defaultValue;
|
||||
this.#client = client;
|
||||
this.#type = type;
|
||||
this.#message = message;
|
||||
this.#defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The type of the dialog.
|
||||
*/
|
||||
type(): Protocol.Page.DialogType {
|
||||
return this._type;
|
||||
return this.#type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The message displayed in the dialog.
|
||||
*/
|
||||
message(): string {
|
||||
return this._message;
|
||||
return this.#message;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +81,7 @@ export class Dialog {
|
||||
* is not a `prompt`.
|
||||
*/
|
||||
defaultValue(): string {
|
||||
return this._defaultValue;
|
||||
return this.#defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,9 +91,9 @@ export class Dialog {
|
||||
* @returns A promise that resolves when the dialog has been accepted.
|
||||
*/
|
||||
async accept(promptText?: string): Promise<void> {
|
||||
assert(!this._handled, 'Cannot accept dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleJavaScriptDialog', {
|
||||
assert(!this.#handled, 'Cannot accept dialog which is already handled!');
|
||||
this.#handled = true;
|
||||
await this.#client.send('Page.handleJavaScriptDialog', {
|
||||
accept: true,
|
||||
promptText: promptText,
|
||||
});
|
||||
@ -103,9 +103,9 @@ export class Dialog {
|
||||
* @returns A promise which will resolve once the dialog has been dismissed
|
||||
*/
|
||||
async dismiss(): Promise<void> {
|
||||
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
|
||||
this._handled = true;
|
||||
await this._client.send('Page.handleJavaScriptDialog', {
|
||||
assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
|
||||
this.#handled = true;
|
||||
await this.#client.send('Page.handleJavaScriptDialog', {
|
||||
accept: false,
|
||||
});
|
||||
}
|
||||
|
@ -18,12 +18,12 @@ import { Viewport } from './PuppeteerViewport.js';
|
||||
import { Protocol } from 'devtools-protocol';
|
||||
|
||||
export class EmulationManager {
|
||||
_client: CDPSession;
|
||||
_emulatingMobile = false;
|
||||
_hasTouch = false;
|
||||
#client: CDPSession;
|
||||
#emulatingMobile = false;
|
||||
#hasTouch = false;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
||||
@ -38,22 +38,22 @@ export class EmulationManager {
|
||||
const hasTouch = viewport.hasTouch || false;
|
||||
|
||||
await Promise.all([
|
||||
this._client.send('Emulation.setDeviceMetricsOverride', {
|
||||
this.#client.send('Emulation.setDeviceMetricsOverride', {
|
||||
mobile,
|
||||
width,
|
||||
height,
|
||||
deviceScaleFactor,
|
||||
screenOrientation,
|
||||
}),
|
||||
this._client.send('Emulation.setTouchEmulationEnabled', {
|
||||
this.#client.send('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: hasTouch,
|
||||
}),
|
||||
]);
|
||||
|
||||
const reloadNeeded =
|
||||
this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
|
||||
this._emulatingMobile = mobile;
|
||||
this._hasTouch = hasTouch;
|
||||
this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
|
||||
this.#emulatingMobile = mobile;
|
||||
this.#hasTouch = hasTouch;
|
||||
return reloadNeeded;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { assert } from './assert.js';
|
||||
import { helper } from './helper.js';
|
||||
import { createJSHandle, JSHandle, ElementHandle } from './JSHandle.js';
|
||||
import { _createJSHandle, JSHandle, ElementHandle } from './JSHandle.js';
|
||||
import { CDPSession } from './Connection.js';
|
||||
import { DOMWorld } from './DOMWorld.js';
|
||||
import { Frame } from './FrameManager.js';
|
||||
@ -137,11 +137,7 @@ export class ExecutionContext {
|
||||
pageFunction: Function | string,
|
||||
...args: unknown[]
|
||||
): Promise<ReturnType> {
|
||||
return await this._evaluateInternal<ReturnType>(
|
||||
true,
|
||||
pageFunction,
|
||||
...args
|
||||
);
|
||||
return await this.#evaluate<ReturnType>(true, pageFunction, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,10 +186,10 @@ export class ExecutionContext {
|
||||
pageFunction: EvaluateHandleFn,
|
||||
...args: SerializableOrJSHandle[]
|
||||
): Promise<HandleType> {
|
||||
return this._evaluateInternal<HandleType>(false, pageFunction, ...args);
|
||||
return this.#evaluate<HandleType>(false, pageFunction, ...args);
|
||||
}
|
||||
|
||||
private async _evaluateInternal<ReturnType>(
|
||||
async #evaluate<ReturnType>(
|
||||
returnByValue: boolean,
|
||||
pageFunction: Function | string,
|
||||
...args: unknown[]
|
||||
@ -224,7 +220,7 @@ export class ExecutionContext {
|
||||
|
||||
return returnByValue
|
||||
? helper.valueFromRemoteObject(remoteObject)
|
||||
: createJSHandle(this, remoteObject);
|
||||
: _createJSHandle(this, remoteObject);
|
||||
}
|
||||
|
||||
if (typeof pageFunction !== 'function')
|
||||
@ -263,8 +259,9 @@ export class ExecutionContext {
|
||||
if (
|
||||
error instanceof TypeError &&
|
||||
error.message.startsWith('Converting circular structure to JSON')
|
||||
)
|
||||
error.message += ' Are you passing a nested JSHandle?';
|
||||
) {
|
||||
error.message += ' Recursive objects are not allowed.';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const { exceptionDetails, result: remoteObject } =
|
||||
@ -275,7 +272,7 @@ export class ExecutionContext {
|
||||
);
|
||||
return returnByValue
|
||||
? helper.valueFromRemoteObject(remoteObject)
|
||||
: createJSHandle(this, remoteObject);
|
||||
: _createJSHandle(this, remoteObject);
|
||||
|
||||
function convertArgument(
|
||||
this: ExecutionContext,
|
||||
@ -355,7 +352,7 @@ export class ExecutionContext {
|
||||
const response = await this._client.send('Runtime.queryObjects', {
|
||||
prototypeObjectId: prototypeHandle._remoteObject.objectId,
|
||||
});
|
||||
return createJSHandle(this, response.objects);
|
||||
return _createJSHandle(this, response.objects);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -368,7 +365,7 @@ export class ExecutionContext {
|
||||
backendNodeId: backendNodeId,
|
||||
executionContextId: this._contextId,
|
||||
});
|
||||
return createJSHandle(this, object) as ElementHandle;
|
||||
return _createJSHandle(this, object) as ElementHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,9 +37,9 @@ import { assert } from './assert.js';
|
||||
* @public
|
||||
*/
|
||||
export class FileChooser {
|
||||
private _element: ElementHandle;
|
||||
private _multiple: boolean;
|
||||
private _handled = false;
|
||||
#element: ElementHandle;
|
||||
#multiple: boolean;
|
||||
#handled = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -48,15 +48,15 @@ export class FileChooser {
|
||||
element: ElementHandle,
|
||||
event: Protocol.Page.FileChooserOpenedEvent
|
||||
) {
|
||||
this._element = element;
|
||||
this._multiple = event.mode !== 'selectSingle';
|
||||
this.#element = element;
|
||||
this.#multiple = event.mode !== 'selectSingle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether file chooser allow for {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple | multiple} file selection.
|
||||
*/
|
||||
isMultiple(): boolean {
|
||||
return this._multiple;
|
||||
return this.#multiple;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,11 +66,11 @@ export class FileChooser {
|
||||
*/
|
||||
async accept(filePaths: string[]): Promise<void> {
|
||||
assert(
|
||||
!this._handled,
|
||||
!this.#handled,
|
||||
'Cannot accept FileChooser which is already handled!'
|
||||
);
|
||||
this._handled = true;
|
||||
await this._element.uploadFile(...filePaths);
|
||||
this.#handled = true;
|
||||
await this.#element.uploadFile(...filePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,9 +78,9 @@ export class FileChooser {
|
||||
*/
|
||||
cancel(): void {
|
||||
assert(
|
||||
!this._handled,
|
||||
!this.#handled,
|
||||
'Cannot cancel FileChooser which is already handled!'
|
||||
);
|
||||
this._handled = true;
|
||||
this.#handled = true;
|
||||
}
|
||||
}
|
||||
|
@ -66,14 +66,28 @@ export const FrameManagerEmittedEvents = {
|
||||
* @internal
|
||||
*/
|
||||
export class FrameManager extends EventEmitter {
|
||||
_client: CDPSession;
|
||||
private _page: Page;
|
||||
private _networkManager: NetworkManager;
|
||||
_timeoutSettings: TimeoutSettings;
|
||||
private _frames = new Map<string, Frame>();
|
||||
private _contextIdToContext = new Map<string, ExecutionContext>();
|
||||
private _isolatedWorlds = new Set<string>();
|
||||
private _mainFrame?: Frame;
|
||||
#page: Page;
|
||||
#networkManager: NetworkManager;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#frames = new Map<string, Frame>();
|
||||
#contextIdToContext = new Map<string, ExecutionContext>();
|
||||
#isolatedWorlds = new Set<string>();
|
||||
#mainFrame?: Frame;
|
||||
#client: CDPSession;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _timeoutSettings(): TimeoutSettings {
|
||||
return this.#timeoutSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _client(): CDPSession {
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
constructor(
|
||||
client: CDPSession,
|
||||
@ -82,64 +96,64 @@ export class FrameManager extends EventEmitter {
|
||||
timeoutSettings: TimeoutSettings
|
||||
) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._page = page;
|
||||
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this.setupEventListeners(this._client);
|
||||
this.#client = client;
|
||||
this.#page = page;
|
||||
this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this.setupEventListeners(this.#client);
|
||||
}
|
||||
|
||||
private setupEventListeners(session: CDPSession) {
|
||||
session.on('Page.frameAttached', (event) => {
|
||||
this._onFrameAttached(session, event.frameId, event.parentFrameId);
|
||||
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
|
||||
});
|
||||
session.on('Page.frameNavigated', (event) => {
|
||||
this._onFrameNavigated(event.frame);
|
||||
this.#onFrameNavigated(event.frame);
|
||||
});
|
||||
session.on('Page.navigatedWithinDocument', (event) => {
|
||||
this._onFrameNavigatedWithinDocument(event.frameId, event.url);
|
||||
this.#onFrameNavigatedWithinDocument(event.frameId, event.url);
|
||||
});
|
||||
session.on(
|
||||
'Page.frameDetached',
|
||||
(event: Protocol.Page.FrameDetachedEvent) => {
|
||||
this._onFrameDetached(
|
||||
this.#onFrameDetached(
|
||||
event.frameId,
|
||||
event.reason as Protocol.Page.FrameDetachedEventReason
|
||||
);
|
||||
}
|
||||
);
|
||||
session.on('Page.frameStartedLoading', (event) => {
|
||||
this._onFrameStartedLoading(event.frameId);
|
||||
this.#onFrameStartedLoading(event.frameId);
|
||||
});
|
||||
session.on('Page.frameStoppedLoading', (event) => {
|
||||
this._onFrameStoppedLoading(event.frameId);
|
||||
this.#onFrameStoppedLoading(event.frameId);
|
||||
});
|
||||
session.on('Runtime.executionContextCreated', (event) => {
|
||||
this._onExecutionContextCreated(event.context, session);
|
||||
this.#onExecutionContextCreated(event.context, session);
|
||||
});
|
||||
session.on('Runtime.executionContextDestroyed', (event) => {
|
||||
this._onExecutionContextDestroyed(event.executionContextId, session);
|
||||
this.#onExecutionContextDestroyed(event.executionContextId, session);
|
||||
});
|
||||
session.on('Runtime.executionContextsCleared', () => {
|
||||
this._onExecutionContextsCleared(session);
|
||||
this.#onExecutionContextsCleared(session);
|
||||
});
|
||||
session.on('Page.lifecycleEvent', (event) => {
|
||||
this._onLifecycleEvent(event);
|
||||
this.#onLifecycleEvent(event);
|
||||
});
|
||||
session.on('Target.attachedToTarget', async (event) => {
|
||||
this._onAttachedToTarget(event);
|
||||
this.#onAttachedToTarget(event);
|
||||
});
|
||||
session.on('Target.detachedFromTarget', async (event) => {
|
||||
this._onDetachedFromTarget(event);
|
||||
this.#onDetachedFromTarget(event);
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(client: CDPSession = this._client): Promise<void> {
|
||||
async initialize(client: CDPSession = this.#client): Promise<void> {
|
||||
try {
|
||||
const result = await Promise.all([
|
||||
client.send('Page.enable'),
|
||||
client.send('Page.getFrameTree'),
|
||||
client !== this._client
|
||||
client !== this.#client
|
||||
? client.send('Target.setAutoAttach', {
|
||||
autoAttach: true,
|
||||
waitForDebuggerOnStart: false,
|
||||
@ -149,15 +163,15 @@ export class FrameManager extends EventEmitter {
|
||||
]);
|
||||
|
||||
const { frameTree } = result[1];
|
||||
this._handleFrameTree(client, frameTree);
|
||||
this.#handleFrameTree(client, frameTree);
|
||||
await Promise.all([
|
||||
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||
client
|
||||
.send('Runtime.enable')
|
||||
.then(() => this._ensureIsolatedWorld(client, UTILITY_WORLD_NAME)),
|
||||
// TODO: Network manager is not aware of OOP iframes yet.
|
||||
client === this._client
|
||||
? this._networkManager.initialize()
|
||||
client === this.#client
|
||||
? this.#networkManager.initialize()
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
} catch (error) {
|
||||
@ -175,7 +189,7 @@ export class FrameManager extends EventEmitter {
|
||||
}
|
||||
|
||||
networkManager(): NetworkManager {
|
||||
return this._networkManager;
|
||||
return this.#networkManager;
|
||||
}
|
||||
|
||||
async navigateFrame(
|
||||
@ -189,14 +203,14 @@ export class FrameManager extends EventEmitter {
|
||||
): Promise<HTTPResponse | null> {
|
||||
assertNoLegacyNavigationOptions(options);
|
||||
const {
|
||||
referer = this._networkManager.extraHTTPHeaders()['referer'],
|
||||
referer = this.#networkManager.extraHTTPHeaders()['referer'],
|
||||
waitUntil = ['load'],
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
timeout = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
|
||||
let error = await Promise.race([
|
||||
navigate(this._client, url, referer, frame._id),
|
||||
navigate(this.#client, url, referer, frame._id),
|
||||
watcher.timeoutOrTerminationPromise(),
|
||||
]);
|
||||
if (!error) {
|
||||
@ -244,7 +258,7 @@ export class FrameManager extends EventEmitter {
|
||||
assertNoLegacyNavigationOptions(options);
|
||||
const {
|
||||
waitUntil = ['load'],
|
||||
timeout = this._timeoutSettings.navigationTimeout(),
|
||||
timeout = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
|
||||
const error = await Promise.race([
|
||||
@ -257,15 +271,13 @@ export class FrameManager extends EventEmitter {
|
||||
return await watcher.navigationResponse();
|
||||
}
|
||||
|
||||
private async _onAttachedToTarget(
|
||||
event: Protocol.Target.AttachedToTargetEvent
|
||||
) {
|
||||
async #onAttachedToTarget(event: Protocol.Target.AttachedToTargetEvent) {
|
||||
if (event.targetInfo.type !== 'iframe') {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = this._frames.get(event.targetInfo.targetId);
|
||||
const connection = Connection.fromSession(this._client);
|
||||
const frame = this.#frames.get(event.targetInfo.targetId);
|
||||
const connection = Connection.fromSession(this.#client);
|
||||
assert(connection);
|
||||
const session = connection.session(event.sessionId);
|
||||
assert(session);
|
||||
@ -274,81 +286,79 @@ export class FrameManager extends EventEmitter {
|
||||
await this.initialize(session);
|
||||
}
|
||||
|
||||
private async _onDetachedFromTarget(
|
||||
event: Protocol.Target.DetachedFromTargetEvent
|
||||
) {
|
||||
async #onDetachedFromTarget(event: Protocol.Target.DetachedFromTargetEvent) {
|
||||
if (!event.targetId) return;
|
||||
const frame = this._frames.get(event.targetId);
|
||||
const frame = this.#frames.get(event.targetId);
|
||||
if (frame && frame.isOOPFrame()) {
|
||||
// When an OOP iframe is removed from the page, it
|
||||
// will only get a Target.detachedFromTarget event.
|
||||
this._removeFramesRecursively(frame);
|
||||
this.#removeFramesRecursively(frame);
|
||||
}
|
||||
}
|
||||
|
||||
_onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
||||
const frame = this._frames.get(event.frameId);
|
||||
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
|
||||
const frame = this.#frames.get(event.frameId);
|
||||
if (!frame) return;
|
||||
frame._onLifecycleEvent(event.loaderId, event.name);
|
||||
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
_onFrameStartedLoading(frameId: string): void {
|
||||
const frame = this._frames.get(frameId);
|
||||
#onFrameStartedLoading(frameId: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
if (!frame) return;
|
||||
frame._onLoadingStarted();
|
||||
}
|
||||
|
||||
_onFrameStoppedLoading(frameId: string): void {
|
||||
const frame = this._frames.get(frameId);
|
||||
#onFrameStoppedLoading(frameId: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
if (!frame) return;
|
||||
frame._onLoadingStopped();
|
||||
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
|
||||
}
|
||||
|
||||
_handleFrameTree(
|
||||
#handleFrameTree(
|
||||
session: CDPSession,
|
||||
frameTree: Protocol.Page.FrameTree
|
||||
): void {
|
||||
if (frameTree.frame.parentId) {
|
||||
this._onFrameAttached(
|
||||
this.#onFrameAttached(
|
||||
session,
|
||||
frameTree.frame.id,
|
||||
frameTree.frame.parentId
|
||||
);
|
||||
}
|
||||
this._onFrameNavigated(frameTree.frame);
|
||||
this.#onFrameNavigated(frameTree.frame);
|
||||
if (!frameTree.childFrames) return;
|
||||
|
||||
for (const child of frameTree.childFrames) {
|
||||
this._handleFrameTree(session, child);
|
||||
this.#handleFrameTree(session, child);
|
||||
}
|
||||
}
|
||||
|
||||
page(): Page {
|
||||
return this._page;
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
mainFrame(): Frame {
|
||||
assert(this._mainFrame, 'Requesting main frame too early!');
|
||||
return this._mainFrame;
|
||||
assert(this.#mainFrame, 'Requesting main frame too early!');
|
||||
return this.#mainFrame;
|
||||
}
|
||||
|
||||
frames(): Frame[] {
|
||||
return Array.from(this._frames.values());
|
||||
return Array.from(this.#frames.values());
|
||||
}
|
||||
|
||||
frame(frameId: string): Frame | null {
|
||||
return this._frames.get(frameId) || null;
|
||||
return this.#frames.get(frameId) || null;
|
||||
}
|
||||
|
||||
_onFrameAttached(
|
||||
#onFrameAttached(
|
||||
session: CDPSession,
|
||||
frameId: string,
|
||||
parentFrameId?: string
|
||||
): void {
|
||||
if (this._frames.has(frameId)) {
|
||||
const frame = this._frames.get(frameId)!;
|
||||
if (this.#frames.has(frameId)) {
|
||||
const frame = this.#frames.get(frameId)!;
|
||||
if (session && frame.isOOPFrame()) {
|
||||
// If an OOP iframes becomes a normal iframe again
|
||||
// it is first attached to the parent page before
|
||||
@ -358,18 +368,18 @@ export class FrameManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
assert(parentFrameId);
|
||||
const parentFrame = this._frames.get(parentFrameId);
|
||||
const parentFrame = this.#frames.get(parentFrameId);
|
||||
assert(parentFrame);
|
||||
const frame = new Frame(this, parentFrame, frameId, session);
|
||||
this._frames.set(frame._id, frame);
|
||||
this.#frames.set(frame._id, frame);
|
||||
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
|
||||
}
|
||||
|
||||
_onFrameNavigated(framePayload: Protocol.Page.Frame): void {
|
||||
#onFrameNavigated(framePayload: Protocol.Page.Frame): void {
|
||||
const isMainFrame = !framePayload.parentId;
|
||||
let frame = isMainFrame
|
||||
? this._mainFrame
|
||||
: this._frames.get(framePayload.id);
|
||||
? this.#mainFrame
|
||||
: this.#frames.get(framePayload.id);
|
||||
assert(
|
||||
isMainFrame || frame,
|
||||
'We either navigate top level or have old version of the navigated frame'
|
||||
@ -378,21 +388,21 @@ export class FrameManager extends EventEmitter {
|
||||
// Detach all child frames first.
|
||||
if (frame) {
|
||||
for (const child of frame.childFrames())
|
||||
this._removeFramesRecursively(child);
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
|
||||
// Update or create main frame.
|
||||
if (isMainFrame) {
|
||||
if (frame) {
|
||||
// Update frame id to retain frame identity on cross-process navigation.
|
||||
this._frames.delete(frame._id);
|
||||
this.#frames.delete(frame._id);
|
||||
frame._id = framePayload.id;
|
||||
} else {
|
||||
// Initial main frame navigation.
|
||||
frame = new Frame(this, null, framePayload.id, this._client);
|
||||
frame = new Frame(this, null, framePayload.id, this.#client);
|
||||
}
|
||||
this._frames.set(framePayload.id, frame);
|
||||
this._mainFrame = frame;
|
||||
this.#frames.set(framePayload.id, frame);
|
||||
this.#mainFrame = frame;
|
||||
}
|
||||
|
||||
// Update frame payload.
|
||||
@ -404,8 +414,8 @@ export class FrameManager extends EventEmitter {
|
||||
|
||||
async _ensureIsolatedWorld(session: CDPSession, name: string): Promise<void> {
|
||||
const key = `${session.id()}:${name}`;
|
||||
if (this._isolatedWorlds.has(key)) return;
|
||||
this._isolatedWorlds.add(key);
|
||||
if (this.#isolatedWorlds.has(key)) return;
|
||||
this.#isolatedWorlds.add(key);
|
||||
|
||||
await session.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
|
||||
@ -414,7 +424,7 @@ export class FrameManager extends EventEmitter {
|
||||
// Frames might be removed before we send this.
|
||||
await Promise.all(
|
||||
this.frames()
|
||||
.filter((frame) => frame._client === session)
|
||||
.filter((frame) => frame._client() === session)
|
||||
.map((frame) =>
|
||||
session
|
||||
.send('Page.createIsolatedWorld', {
|
||||
@ -427,41 +437,41 @@ export class FrameManager extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
_onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
||||
const frame = this._frames.get(frameId);
|
||||
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
|
||||
const frame = this.#frames.get(frameId);
|
||||
if (!frame) return;
|
||||
frame._navigatedWithinDocument(url);
|
||||
this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame);
|
||||
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
|
||||
}
|
||||
|
||||
_onFrameDetached(
|
||||
#onFrameDetached(
|
||||
frameId: string,
|
||||
reason: Protocol.Page.FrameDetachedEventReason
|
||||
): void {
|
||||
const frame = this._frames.get(frameId);
|
||||
const frame = this.#frames.get(frameId);
|
||||
if (reason === 'remove') {
|
||||
// Only remove the frame if the reason for the detached event is
|
||||
// an actual removement of the frame.
|
||||
// For frames that become OOP iframes, the reason would be 'swap'.
|
||||
if (frame) this._removeFramesRecursively(frame);
|
||||
if (frame) this.#removeFramesRecursively(frame);
|
||||
} else if (reason === 'swap') {
|
||||
this.emit(FrameManagerEmittedEvents.FrameSwapped, frame);
|
||||
}
|
||||
}
|
||||
|
||||
_onExecutionContextCreated(
|
||||
#onExecutionContextCreated(
|
||||
contextPayload: Protocol.Runtime.ExecutionContextDescription,
|
||||
session: CDPSession
|
||||
): void {
|
||||
const auxData = contextPayload.auxData as { frameId?: string } | undefined;
|
||||
const frameId = auxData && auxData.frameId;
|
||||
const frame =
|
||||
typeof frameId === 'string' ? this._frames.get(frameId) : undefined;
|
||||
typeof frameId === 'string' ? this.#frames.get(frameId) : undefined;
|
||||
let world: DOMWorld | undefined;
|
||||
if (frame) {
|
||||
// Only care about execution contexts created for the current session.
|
||||
if (frame._client !== session) return;
|
||||
if (frame._client() !== session) return;
|
||||
|
||||
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
|
||||
world = frame._mainWorld;
|
||||
@ -476,51 +486,51 @@ export class FrameManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
const context = new ExecutionContext(
|
||||
frame?._client || this._client,
|
||||
frame?._client() || this.#client,
|
||||
contextPayload,
|
||||
world
|
||||
);
|
||||
if (world) world._setContext(context);
|
||||
const key = `${session.id()}:${contextPayload.id}`;
|
||||
this._contextIdToContext.set(key, context);
|
||||
this.#contextIdToContext.set(key, context);
|
||||
}
|
||||
|
||||
private _onExecutionContextDestroyed(
|
||||
#onExecutionContextDestroyed(
|
||||
executionContextId: number,
|
||||
session: CDPSession
|
||||
): void {
|
||||
const key = `${session.id()}:${executionContextId}`;
|
||||
const context = this._contextIdToContext.get(key);
|
||||
const context = this.#contextIdToContext.get(key);
|
||||
if (!context) return;
|
||||
this._contextIdToContext.delete(key);
|
||||
this.#contextIdToContext.delete(key);
|
||||
if (context._world) context._world._setContext(null);
|
||||
}
|
||||
|
||||
private _onExecutionContextsCleared(session: CDPSession): void {
|
||||
for (const [key, context] of this._contextIdToContext.entries()) {
|
||||
#onExecutionContextsCleared(session: CDPSession): void {
|
||||
for (const [key, context] of this.#contextIdToContext.entries()) {
|
||||
// Make sure to only clear execution contexts that belong
|
||||
// to the current session.
|
||||
if (context._client !== session) continue;
|
||||
if (context._world) context._world._setContext(null);
|
||||
this._contextIdToContext.delete(key);
|
||||
this.#contextIdToContext.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
executionContextById(
|
||||
contextId: number,
|
||||
session: CDPSession = this._client
|
||||
session: CDPSession = this.#client
|
||||
): ExecutionContext {
|
||||
const key = `${session.id()}:${contextId}`;
|
||||
const context = this._contextIdToContext.get(key);
|
||||
const context = this.#contextIdToContext.get(key);
|
||||
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
|
||||
return context;
|
||||
}
|
||||
|
||||
private _removeFramesRecursively(frame: Frame): void {
|
||||
#removeFramesRecursively(frame: Frame): void {
|
||||
for (const child of frame.childFrames())
|
||||
this._removeFramesRecursively(child);
|
||||
this.#removeFramesRecursively(child);
|
||||
frame._detach();
|
||||
this._frames.delete(frame._id);
|
||||
this.#frames.delete(frame._id);
|
||||
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
|
||||
}
|
||||
}
|
||||
@ -646,18 +656,19 @@ export interface FrameAddStyleTagOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Frame {
|
||||
#parentFrame: Frame | null;
|
||||
#url = '';
|
||||
#detached = false;
|
||||
#client!: CDPSession;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_frameManager: FrameManager;
|
||||
private _parentFrame: Frame | null;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_id: string;
|
||||
|
||||
private _url = '';
|
||||
private _detached = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -670,7 +681,6 @@ export class Frame {
|
||||
* @internal
|
||||
*/
|
||||
_hasStartedLoading = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -687,10 +697,6 @@ export class Frame {
|
||||
* @internal
|
||||
*/
|
||||
_childFrames: Set<Frame>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_client!: CDPSession;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -702,15 +708,15 @@ export class Frame {
|
||||
client: CDPSession
|
||||
) {
|
||||
this._frameManager = frameManager;
|
||||
this._parentFrame = parentFrame ?? null;
|
||||
this._url = '';
|
||||
this.#parentFrame = parentFrame ?? null;
|
||||
this.#url = '';
|
||||
this._id = frameId;
|
||||
this._detached = false;
|
||||
this.#detached = false;
|
||||
|
||||
this._loaderId = '';
|
||||
|
||||
this._childFrames = new Set();
|
||||
if (this._parentFrame) this._parentFrame._childFrames.add(this);
|
||||
if (this.#parentFrame) this.#parentFrame._childFrames.add(this);
|
||||
|
||||
this._updateClient(client);
|
||||
}
|
||||
@ -719,15 +725,15 @@ export class Frame {
|
||||
* @internal
|
||||
*/
|
||||
_updateClient(client: CDPSession): void {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
this._mainWorld = new DOMWorld(
|
||||
this._client,
|
||||
this.#client,
|
||||
this._frameManager,
|
||||
this,
|
||||
this._frameManager._timeoutSettings
|
||||
);
|
||||
this._secondaryWorld = new DOMWorld(
|
||||
this._client,
|
||||
this.#client,
|
||||
this._frameManager,
|
||||
this,
|
||||
this._frameManager._timeoutSettings
|
||||
@ -740,7 +746,7 @@ export class Frame {
|
||||
* @returns `true` if the frame is an OOP frame, or `false` otherwise.
|
||||
*/
|
||||
isOOPFrame(): boolean {
|
||||
return this._client !== this._frameManager._client;
|
||||
return this.#client !== this._frameManager._client;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -825,8 +831,8 @@ export class Frame {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
client(): CDPSession {
|
||||
return this._client;
|
||||
_client(): CDPSession {
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1008,14 +1014,14 @@ export class Frame {
|
||||
* @returns the frame's URL.
|
||||
*/
|
||||
url(): string {
|
||||
return this._url;
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the parent `Frame`, if any. Detached and main frames return `null`.
|
||||
*/
|
||||
parentFrame(): Frame | null {
|
||||
return this._parentFrame;
|
||||
return this.#parentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1029,7 +1035,7 @@ export class Frame {
|
||||
* @returns `true` if the frame has been detached, or `false` otherwise.
|
||||
*/
|
||||
isDetached(): boolean {
|
||||
return this._detached;
|
||||
return this.#detached;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1407,14 +1413,14 @@ export class Frame {
|
||||
*/
|
||||
_navigated(framePayload: Protocol.Page.Frame): void {
|
||||
this._name = framePayload.name;
|
||||
this._url = `${framePayload.url}${framePayload.urlFragment || ''}`;
|
||||
this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_navigatedWithinDocument(url: string): void {
|
||||
this._url = url;
|
||||
this.#url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1447,11 +1453,11 @@ export class Frame {
|
||||
* @internal
|
||||
*/
|
||||
_detach(): void {
|
||||
this._detached = true;
|
||||
this.#detached = true;
|
||||
this._mainWorld._detach();
|
||||
this._secondaryWorld._detach();
|
||||
if (this._parentFrame) this._parentFrame._childFrames.delete(this);
|
||||
this._parentFrame = null;
|
||||
if (this.#parentFrame) this.#parentFrame._childFrames.delete(this);
|
||||
this.#parentFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,25 +138,25 @@ export class HTTPRequest {
|
||||
*/
|
||||
_redirectChain: HTTPRequest[];
|
||||
|
||||
private _client: CDPSession;
|
||||
private _isNavigationRequest: boolean;
|
||||
private _allowInterception: boolean;
|
||||
private _interceptionHandled = false;
|
||||
private _url: string;
|
||||
private _resourceType: ResourceType;
|
||||
#client: CDPSession;
|
||||
#isNavigationRequest: boolean;
|
||||
#allowInterception: boolean;
|
||||
#interceptionHandled = false;
|
||||
#url: string;
|
||||
#resourceType: ResourceType;
|
||||
|
||||
private _method: string;
|
||||
private _postData?: string;
|
||||
private _headers: Record<string, string> = {};
|
||||
private _frame: Frame | null;
|
||||
private _continueRequestOverrides: ContinueRequestOverrides;
|
||||
private _responseForRequest: Partial<ResponseForRequest> | null = null;
|
||||
private _abortErrorReason: Protocol.Network.ErrorReason | null = null;
|
||||
private _interceptResolutionState: InterceptResolutionState = {
|
||||
#method: string;
|
||||
#postData?: string;
|
||||
#headers: Record<string, string> = {};
|
||||
#frame: Frame | null;
|
||||
#continueRequestOverrides: ContinueRequestOverrides;
|
||||
#responseForRequest: Partial<ResponseForRequest> | null = null;
|
||||
#abortErrorReason: Protocol.Network.ErrorReason | null = null;
|
||||
#interceptResolutionState: InterceptResolutionState = {
|
||||
action: InterceptResolutionAction.None,
|
||||
};
|
||||
private _interceptHandlers: Array<() => void | PromiseLike<any>>;
|
||||
private _initiator: Protocol.Network.Initiator;
|
||||
#interceptHandlers: Array<() => void | PromiseLike<any>>;
|
||||
#initiator: Protocol.Network.Initiator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -169,31 +169,31 @@ export class HTTPRequest {
|
||||
event: Protocol.Network.RequestWillBeSentEvent,
|
||||
redirectChain: HTTPRequest[]
|
||||
) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
this._requestId = event.requestId;
|
||||
this._isNavigationRequest =
|
||||
this.#isNavigationRequest =
|
||||
event.requestId === event.loaderId && event.type === 'Document';
|
||||
this._interceptionId = interceptionId;
|
||||
this._allowInterception = allowInterception;
|
||||
this._url = event.request.url;
|
||||
this._resourceType = (event.type || 'other').toLowerCase() as ResourceType;
|
||||
this._method = event.request.method;
|
||||
this._postData = event.request.postData;
|
||||
this._frame = frame;
|
||||
this.#allowInterception = allowInterception;
|
||||
this.#url = event.request.url;
|
||||
this.#resourceType = (event.type || 'other').toLowerCase() as ResourceType;
|
||||
this.#method = event.request.method;
|
||||
this.#postData = event.request.postData;
|
||||
this.#frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
this._continueRequestOverrides = {};
|
||||
this._interceptHandlers = [];
|
||||
this._initiator = event.initiator;
|
||||
this.#continueRequestOverrides = {};
|
||||
this.#interceptHandlers = [];
|
||||
this.#initiator = event.initiator;
|
||||
|
||||
for (const [key, value] of Object.entries(event.request.headers))
|
||||
this._headers[key.toLowerCase()] = value;
|
||||
this.#headers[key.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the URL of the request
|
||||
*/
|
||||
url(): string {
|
||||
return this._url;
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -202,8 +202,8 @@ export class HTTPRequest {
|
||||
* `respond()` aren't called).
|
||||
*/
|
||||
continueRequestOverrides(): ContinueRequestOverrides {
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
return this._continueRequestOverrides;
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#continueRequestOverrides;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -211,16 +211,16 @@ export class HTTPRequest {
|
||||
* interception is allowed to respond (ie, `abort()` is not called).
|
||||
*/
|
||||
responseForRequest(): Partial<ResponseForRequest> | null {
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
return this._responseForRequest;
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#responseForRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the most recent reason for aborting the request
|
||||
*/
|
||||
abortErrorReason(): Protocol.Network.ErrorReason | null {
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
return this._abortErrorReason;
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
return this.#abortErrorReason;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -235,11 +235,11 @@ export class HTTPRequest {
|
||||
* `disabled`, `none`, or `already-handled`.
|
||||
*/
|
||||
interceptResolutionState(): InterceptResolutionState {
|
||||
if (!this._allowInterception)
|
||||
if (!this.#allowInterception)
|
||||
return { action: InterceptResolutionAction.Disabled };
|
||||
if (this._interceptionHandled)
|
||||
if (this.#interceptionHandled)
|
||||
return { action: InterceptResolutionAction.AlreadyHandled };
|
||||
return { ...this._interceptResolutionState };
|
||||
return { ...this.#interceptResolutionState };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,7 +247,7 @@ export class HTTPRequest {
|
||||
* `false` otherwise.
|
||||
*/
|
||||
isInterceptResolutionHandled(): boolean {
|
||||
return this._interceptionHandled;
|
||||
return this.#interceptionHandled;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -259,7 +259,7 @@ export class HTTPRequest {
|
||||
enqueueInterceptAction(
|
||||
pendingHandler: () => void | PromiseLike<unknown>
|
||||
): void {
|
||||
this._interceptHandlers.push(pendingHandler);
|
||||
this.#interceptHandlers.push(pendingHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,21 +267,21 @@ export class HTTPRequest {
|
||||
* the request interception.
|
||||
*/
|
||||
async finalizeInterceptions(): Promise<void> {
|
||||
await this._interceptHandlers.reduce(
|
||||
await this.#interceptHandlers.reduce(
|
||||
(promiseChain, interceptAction) => promiseChain.then(interceptAction),
|
||||
Promise.resolve()
|
||||
);
|
||||
const { action } = this.interceptResolutionState();
|
||||
switch (action) {
|
||||
case 'abort':
|
||||
return this._abort(this._abortErrorReason);
|
||||
return this.#abort(this.#abortErrorReason);
|
||||
case 'respond':
|
||||
if (this._responseForRequest === null) {
|
||||
if (this.#responseForRequest === null) {
|
||||
throw new Error('Response is missing for the interception');
|
||||
}
|
||||
return this._respond(this._responseForRequest);
|
||||
return this.#respond(this.#responseForRequest);
|
||||
case 'continue':
|
||||
return this._continue(this._continueRequestOverrides);
|
||||
return this.#continue(this.#continueRequestOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,21 +290,21 @@ export class HTTPRequest {
|
||||
* engine.
|
||||
*/
|
||||
resourceType(): ResourceType {
|
||||
return this._resourceType;
|
||||
return this.#resourceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the method used (`GET`, `POST`, etc.)
|
||||
*/
|
||||
method(): string {
|
||||
return this._method;
|
||||
return this.#method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the request's post body, if any.
|
||||
*/
|
||||
postData(): string | undefined {
|
||||
return this._postData;
|
||||
return this.#postData;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -312,7 +312,7 @@ export class HTTPRequest {
|
||||
* header names are lower-case.
|
||||
*/
|
||||
headers(): Record<string, string> {
|
||||
return this._headers;
|
||||
return this.#headers;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,21 +328,21 @@ export class HTTPRequest {
|
||||
* error pages.
|
||||
*/
|
||||
frame(): Frame | null {
|
||||
return this._frame;
|
||||
return this.#frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the request is the driver of the current frame's navigation.
|
||||
*/
|
||||
isNavigationRequest(): boolean {
|
||||
return this._isNavigationRequest;
|
||||
return this.#isNavigationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the initiator of the request.
|
||||
*/
|
||||
initiator(): Protocol.Network.Initiator {
|
||||
return this._initiator;
|
||||
return this.#initiator;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -436,41 +436,39 @@ export class HTTPRequest {
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this._url.startsWith('data:')) return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
if (this.#url.startsWith('data:')) return;
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return this._continue(overrides);
|
||||
return this.#continue(overrides);
|
||||
}
|
||||
this._continueRequestOverrides = overrides;
|
||||
this.#continueRequestOverrides = overrides;
|
||||
if (
|
||||
this._interceptResolutionState.priority === undefined ||
|
||||
priority > this._interceptResolutionState.priority
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority > this.#interceptResolutionState.priority
|
||||
) {
|
||||
this._interceptResolutionState = {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Continue,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this._interceptResolutionState.priority) {
|
||||
if (priority === this.#interceptResolutionState.priority) {
|
||||
if (
|
||||
this._interceptResolutionState.action === 'abort' ||
|
||||
this._interceptResolutionState.action === 'respond'
|
||||
this.#interceptResolutionState.action === 'abort' ||
|
||||
this.#interceptResolutionState.action === 'respond'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._interceptResolutionState.action =
|
||||
this.#interceptResolutionState.action =
|
||||
InterceptResolutionAction.Continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private async _continue(
|
||||
overrides: ContinueRequestOverrides = {}
|
||||
): Promise<void> {
|
||||
async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
|
||||
const { url, method, postData, headers } = overrides;
|
||||
this._interceptionHandled = true;
|
||||
this.#interceptionHandled = true;
|
||||
|
||||
const postDataBinaryBase64 = postData
|
||||
? Buffer.from(postData).toString('base64')
|
||||
@ -480,7 +478,7 @@ export class HTTPRequest {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest'
|
||||
);
|
||||
await this._client
|
||||
await this.#client
|
||||
.send('Fetch.continueRequest', {
|
||||
requestId: this._interceptionId,
|
||||
url,
|
||||
@ -489,7 +487,7 @@ export class HTTPRequest {
|
||||
headers: headers ? headersArray(headers) : undefined,
|
||||
})
|
||||
.catch((error) => {
|
||||
this._interceptionHandled = false;
|
||||
this.#interceptionHandled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
@ -530,33 +528,33 @@ export class HTTPRequest {
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Mocking responses for dataURL requests is not currently supported.
|
||||
if (this._url.startsWith('data:')) return;
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
if (this.#url.startsWith('data:')) return;
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return this._respond(response);
|
||||
return this.#respond(response);
|
||||
}
|
||||
this._responseForRequest = response;
|
||||
this.#responseForRequest = response;
|
||||
if (
|
||||
this._interceptResolutionState.priority === undefined ||
|
||||
priority > this._interceptResolutionState.priority
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority > this.#interceptResolutionState.priority
|
||||
) {
|
||||
this._interceptResolutionState = {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Respond,
|
||||
priority,
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (priority === this._interceptResolutionState.priority) {
|
||||
if (this._interceptResolutionState.action === 'abort') {
|
||||
if (priority === this.#interceptResolutionState.priority) {
|
||||
if (this.#interceptResolutionState.action === 'abort') {
|
||||
return;
|
||||
}
|
||||
this._interceptResolutionState.action = InterceptResolutionAction.Respond;
|
||||
this.#interceptResolutionState.action = InterceptResolutionAction.Respond;
|
||||
}
|
||||
}
|
||||
|
||||
private async _respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this._interceptionHandled = true;
|
||||
async #respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this.#interceptionHandled = true;
|
||||
|
||||
const responseBody: Buffer | null =
|
||||
response.body && helper.isString(response.body)
|
||||
@ -585,7 +583,7 @@ export class HTTPRequest {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest'
|
||||
);
|
||||
await this._client
|
||||
await this.#client
|
||||
.send('Fetch.fulfillRequest', {
|
||||
requestId: this._interceptionId,
|
||||
responseCode: status,
|
||||
@ -594,7 +592,7 @@ export class HTTPRequest {
|
||||
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
})
|
||||
.catch((error) => {
|
||||
this._interceptionHandled = false;
|
||||
this.#interceptionHandled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
@ -617,20 +615,20 @@ export class HTTPRequest {
|
||||
priority?: number
|
||||
): Promise<void> {
|
||||
// Request interception is not supported for data: urls.
|
||||
if (this._url.startsWith('data:')) return;
|
||||
if (this.#url.startsWith('data:')) return;
|
||||
const errorReason = errorReasons[errorCode];
|
||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||
assert(this.#allowInterception, 'Request Interception is not enabled!');
|
||||
assert(!this.#interceptionHandled, 'Request is already handled!');
|
||||
if (priority === undefined) {
|
||||
return this._abort(errorReason);
|
||||
return this.#abort(errorReason);
|
||||
}
|
||||
this._abortErrorReason = errorReason;
|
||||
this.#abortErrorReason = errorReason;
|
||||
if (
|
||||
this._interceptResolutionState.priority === undefined ||
|
||||
priority >= this._interceptResolutionState.priority
|
||||
this.#interceptResolutionState.priority === undefined ||
|
||||
priority >= this.#interceptResolutionState.priority
|
||||
) {
|
||||
this._interceptResolutionState = {
|
||||
this.#interceptResolutionState = {
|
||||
action: InterceptResolutionAction.Abort,
|
||||
priority,
|
||||
};
|
||||
@ -638,15 +636,15 @@ export class HTTPRequest {
|
||||
}
|
||||
}
|
||||
|
||||
private async _abort(
|
||||
async #abort(
|
||||
errorReason: Protocol.Network.ErrorReason | null
|
||||
): Promise<void> {
|
||||
this._interceptionHandled = true;
|
||||
this.#interceptionHandled = true;
|
||||
if (this._interceptionId === undefined)
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
|
||||
);
|
||||
await this._client
|
||||
await this.#client
|
||||
.send('Fetch.failRequest', {
|
||||
requestId: this._interceptionId,
|
||||
errorReason: errorReason || 'Failed',
|
||||
|
@ -44,20 +44,20 @@ interface CDPSession extends EventEmitter {
|
||||
* @public
|
||||
*/
|
||||
export class HTTPResponse {
|
||||
private _client: CDPSession;
|
||||
private _request: HTTPRequest;
|
||||
private _contentPromise: Promise<Buffer> | null = null;
|
||||
private _bodyLoadedPromise: Promise<Error | void>;
|
||||
private _bodyLoadedPromiseFulfill: (err: Error | void) => void = () => {};
|
||||
private _remoteAddress: RemoteAddress;
|
||||
private _status: number;
|
||||
private _statusText: string;
|
||||
private _url: string;
|
||||
private _fromDiskCache: boolean;
|
||||
private _fromServiceWorker: boolean;
|
||||
private _headers: Record<string, string> = {};
|
||||
private _securityDetails: SecurityDetails | null;
|
||||
private _timing: Protocol.Network.ResourceTiming | null;
|
||||
#client: CDPSession;
|
||||
#request: HTTPRequest;
|
||||
#contentPromise: Promise<Buffer> | null = null;
|
||||
#bodyLoadedPromise: Promise<Error | void>;
|
||||
#bodyLoadedPromiseFulfill: (err: Error | void) => void = () => {};
|
||||
#remoteAddress: RemoteAddress;
|
||||
#status: number;
|
||||
#statusText: string;
|
||||
#url: string;
|
||||
#fromDiskCache: boolean;
|
||||
#fromServiceWorker: boolean;
|
||||
#headers: Record<string, string> = {};
|
||||
#securityDetails: SecurityDetails | null;
|
||||
#timing: Protocol.Network.ResourceTiming | null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -68,40 +68,37 @@ export class HTTPResponse {
|
||||
responsePayload: Protocol.Network.Response,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
|
||||
) {
|
||||
this._client = client;
|
||||
this._request = request;
|
||||
this.#client = client;
|
||||
this.#request = request;
|
||||
|
||||
this._bodyLoadedPromise = new Promise((fulfill) => {
|
||||
this._bodyLoadedPromiseFulfill = fulfill;
|
||||
this.#bodyLoadedPromise = new Promise((fulfill) => {
|
||||
this.#bodyLoadedPromiseFulfill = fulfill;
|
||||
});
|
||||
|
||||
this._remoteAddress = {
|
||||
this.#remoteAddress = {
|
||||
ip: responsePayload.remoteIPAddress,
|
||||
port: responsePayload.remotePort,
|
||||
};
|
||||
this._statusText =
|
||||
this._parseStatusTextFromExtrInfo(extraInfo) ||
|
||||
this.#statusText =
|
||||
this.#parseStatusTextFromExtrInfo(extraInfo) ||
|
||||
responsePayload.statusText;
|
||||
this._url = request.url();
|
||||
this._fromDiskCache = !!responsePayload.fromDiskCache;
|
||||
this._fromServiceWorker = !!responsePayload.fromServiceWorker;
|
||||
this.#url = request.url();
|
||||
this.#fromDiskCache = !!responsePayload.fromDiskCache;
|
||||
this.#fromServiceWorker = !!responsePayload.fromServiceWorker;
|
||||
|
||||
this._status = extraInfo ? extraInfo.statusCode : responsePayload.status;
|
||||
this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status;
|
||||
const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
this._headers[key.toLowerCase()] = value;
|
||||
this.#headers[key.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
this._securityDetails = responsePayload.securityDetails
|
||||
this.#securityDetails = responsePayload.securityDetails
|
||||
? new SecurityDetails(responsePayload.securityDetails)
|
||||
: null;
|
||||
this._timing = responsePayload.timing || null;
|
||||
this.#timing = responsePayload.timing || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_parseStatusTextFromExtrInfo(
|
||||
#parseStatusTextFromExtrInfo(
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
|
||||
): string | undefined {
|
||||
if (!extraInfo || !extraInfo.headersText) return;
|
||||
@ -119,9 +116,9 @@ export class HTTPResponse {
|
||||
*/
|
||||
_resolveBody(err: Error | null): void {
|
||||
if (err) {
|
||||
return this._bodyLoadedPromiseFulfill(err);
|
||||
return this.#bodyLoadedPromiseFulfill(err);
|
||||
}
|
||||
return this._bodyLoadedPromiseFulfill();
|
||||
return this.#bodyLoadedPromiseFulfill();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,14 +126,14 @@ export class HTTPResponse {
|
||||
* server.
|
||||
*/
|
||||
remoteAddress(): RemoteAddress {
|
||||
return this._remoteAddress;
|
||||
return this.#remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The URL of the response.
|
||||
*/
|
||||
url(): string {
|
||||
return this._url;
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,14 +141,14 @@ export class HTTPResponse {
|
||||
*/
|
||||
ok(): boolean {
|
||||
// TODO: document === 0 case?
|
||||
return this._status === 0 || (this._status >= 200 && this._status <= 299);
|
||||
return this.#status === 0 || (this.#status >= 200 && this.#status <= 299);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The status code of the response (e.g., 200 for a success).
|
||||
*/
|
||||
status(): number {
|
||||
return this._status;
|
||||
return this.#status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -159,7 +156,7 @@ export class HTTPResponse {
|
||||
* success).
|
||||
*/
|
||||
statusText(): string {
|
||||
return this._statusText;
|
||||
return this.#statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,7 +164,7 @@ export class HTTPResponse {
|
||||
* header names are lower-case.
|
||||
*/
|
||||
headers(): Record<string, string> {
|
||||
return this._headers;
|
||||
return this.#headers;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,26 +172,26 @@ export class HTTPResponse {
|
||||
* secure connection, or `null` otherwise.
|
||||
*/
|
||||
securityDetails(): SecurityDetails | null {
|
||||
return this._securityDetails;
|
||||
return this.#securityDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Timing information related to the response.
|
||||
*/
|
||||
timing(): Protocol.Network.ResourceTiming | null {
|
||||
return this._timing;
|
||||
return this.#timing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves to a buffer with response body.
|
||||
*/
|
||||
buffer(): Promise<Buffer> {
|
||||
if (!this._contentPromise) {
|
||||
this._contentPromise = this._bodyLoadedPromise.then(async (error) => {
|
||||
if (!this.#contentPromise) {
|
||||
this.#contentPromise = this.#bodyLoadedPromise.then(async (error) => {
|
||||
if (error) throw error;
|
||||
try {
|
||||
const response = await this._client.send('Network.getResponseBody', {
|
||||
requestId: this._request._requestId,
|
||||
const response = await this.#client.send('Network.getResponseBody', {
|
||||
requestId: this.#request._requestId,
|
||||
});
|
||||
return Buffer.from(
|
||||
response.body,
|
||||
@ -214,7 +211,7 @@ export class HTTPResponse {
|
||||
}
|
||||
});
|
||||
}
|
||||
return this._contentPromise;
|
||||
return this.#contentPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -243,7 +240,7 @@ export class HTTPResponse {
|
||||
* @returns A matching {@link HTTPRequest} object.
|
||||
*/
|
||||
request(): HTTPRequest {
|
||||
return this._request;
|
||||
return this.#request;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,14 +248,14 @@ export class HTTPResponse {
|
||||
* cache or memory cache.
|
||||
*/
|
||||
fromCache(): boolean {
|
||||
return this._fromDiskCache || this._request._fromMemoryCache;
|
||||
return this.#fromDiskCache || this.#request._fromMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the response was served by a service worker.
|
||||
*/
|
||||
fromServiceWorker(): boolean {
|
||||
return this._fromServiceWorker;
|
||||
return this.#fromServiceWorker;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,6 +263,6 @@ export class HTTPResponse {
|
||||
* navigating to error pages.
|
||||
*/
|
||||
frame(): Frame | null {
|
||||
return this._request.frame();
|
||||
return this.#request.frame();
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,11 @@
|
||||
|
||||
import { assert } from './assert.js';
|
||||
import { CDPSession } from './Connection.js';
|
||||
import { keyDefinitions, KeyDefinition, KeyInput } from './USKeyboardLayout.js';
|
||||
import {
|
||||
_keyDefinitions,
|
||||
KeyDefinition,
|
||||
KeyInput,
|
||||
} from './USKeyboardLayout.js';
|
||||
import { Protocol } from 'devtools-protocol';
|
||||
import { Point } from './JSHandle.js';
|
||||
|
||||
@ -64,14 +68,19 @@ type KeyDescription = Required<
|
||||
* @public
|
||||
*/
|
||||
export class Keyboard {
|
||||
private _client: CDPSession;
|
||||
/** @internal */
|
||||
_modifiers = 0;
|
||||
private _pressedKeys = new Set<string>();
|
||||
#client: CDPSession;
|
||||
#pressedKeys = new Set<string>();
|
||||
|
||||
/** @internal */
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_modifiers = 0;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,14 +112,14 @@ export class Keyboard {
|
||||
key: KeyInput,
|
||||
options: { text?: string } = { text: undefined }
|
||||
): Promise<void> {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
const description = this.#keyDescriptionForString(key);
|
||||
|
||||
const autoRepeat = this._pressedKeys.has(description.code);
|
||||
this._pressedKeys.add(description.code);
|
||||
this._modifiers |= this._modifierBit(description.key);
|
||||
const autoRepeat = this.#pressedKeys.has(description.code);
|
||||
this.#pressedKeys.add(description.code);
|
||||
this._modifiers |= this.#modifierBit(description.key);
|
||||
|
||||
const text = options.text === undefined ? description.text : options.text;
|
||||
await this._client.send('Input.dispatchKeyEvent', {
|
||||
await this.#client.send('Input.dispatchKeyEvent', {
|
||||
type: text ? 'keyDown' : 'rawKeyDown',
|
||||
modifiers: this._modifiers,
|
||||
windowsVirtualKeyCode: description.keyCode,
|
||||
@ -124,7 +133,7 @@ export class Keyboard {
|
||||
});
|
||||
}
|
||||
|
||||
private _modifierBit(key: string): number {
|
||||
#modifierBit(key: string): number {
|
||||
if (key === 'Alt') return 1;
|
||||
if (key === 'Control') return 2;
|
||||
if (key === 'Meta') return 4;
|
||||
@ -132,7 +141,7 @@ export class Keyboard {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private _keyDescriptionForString(keyString: KeyInput): KeyDescription {
|
||||
#keyDescriptionForString(keyString: KeyInput): KeyDescription {
|
||||
const shift = this._modifiers & 8;
|
||||
const description = {
|
||||
key: '',
|
||||
@ -142,7 +151,7 @@ export class Keyboard {
|
||||
location: 0,
|
||||
};
|
||||
|
||||
const definition = keyDefinitions[keyString];
|
||||
const definition = _keyDefinitions[keyString];
|
||||
assert(definition, `Unknown key: "${keyString}"`);
|
||||
|
||||
if (definition.key) description.key = definition.key;
|
||||
@ -175,11 +184,11 @@ export class Keyboard {
|
||||
* for a list of all key names.
|
||||
*/
|
||||
async up(key: KeyInput): Promise<void> {
|
||||
const description = this._keyDescriptionForString(key);
|
||||
const description = this.#keyDescriptionForString(key);
|
||||
|
||||
this._modifiers &= ~this._modifierBit(description.key);
|
||||
this._pressedKeys.delete(description.code);
|
||||
await this._client.send('Input.dispatchKeyEvent', {
|
||||
this._modifiers &= ~this.#modifierBit(description.key);
|
||||
this.#pressedKeys.delete(description.code);
|
||||
await this.#client.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
modifiers: this._modifiers,
|
||||
key: description.key,
|
||||
@ -205,11 +214,11 @@ export class Keyboard {
|
||||
* @param char - Character to send into the page.
|
||||
*/
|
||||
async sendCharacter(char: string): Promise<void> {
|
||||
await this._client.send('Input.insertText', { text: char });
|
||||
await this.#client.send('Input.insertText', { text: char });
|
||||
}
|
||||
|
||||
private charIsKey(char: string): char is KeyInput {
|
||||
return !!keyDefinitions[char as KeyInput];
|
||||
return !!_keyDefinitions[char as KeyInput];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,18 +365,18 @@ export interface MouseWheelOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Mouse {
|
||||
private _client: CDPSession;
|
||||
private _keyboard: Keyboard;
|
||||
private _x = 0;
|
||||
private _y = 0;
|
||||
private _button: MouseButton | 'none' = 'none';
|
||||
#client: CDPSession;
|
||||
#keyboard: Keyboard;
|
||||
#x = 0;
|
||||
#y = 0;
|
||||
#button: MouseButton | 'none' = 'none';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession, keyboard: Keyboard) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this.#client = client;
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -383,17 +392,17 @@ export class Mouse {
|
||||
options: { steps?: number } = {}
|
||||
): Promise<void> {
|
||||
const { steps = 1 } = options;
|
||||
const fromX = this._x,
|
||||
fromY = this._y;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
const fromX = this.#x,
|
||||
fromY = this.#y;
|
||||
this.#x = x;
|
||||
this.#y = y;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await this._client.send('Input.dispatchMouseEvent', {
|
||||
await this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved',
|
||||
button: this._button,
|
||||
x: fromX + (this._x - fromX) * (i / steps),
|
||||
y: fromY + (this._y - fromY) * (i / steps),
|
||||
modifiers: this._keyboard._modifiers,
|
||||
button: this.#button,
|
||||
x: fromX + (this.#x - fromX) * (i / steps),
|
||||
y: fromY + (this.#y - fromY) * (i / steps),
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -428,13 +437,13 @@ export class Mouse {
|
||||
*/
|
||||
async down(options: MouseOptions = {}): Promise<void> {
|
||||
const { button = 'left', clickCount = 1 } = options;
|
||||
this._button = button;
|
||||
await this._client.send('Input.dispatchMouseEvent', {
|
||||
this.#button = button;
|
||||
await this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed',
|
||||
button,
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
x: this.#x,
|
||||
y: this.#y,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
clickCount,
|
||||
});
|
||||
}
|
||||
@ -445,13 +454,13 @@ export class Mouse {
|
||||
*/
|
||||
async up(options: MouseOptions = {}): Promise<void> {
|
||||
const { button = 'left', clickCount = 1 } = options;
|
||||
this._button = 'none';
|
||||
await this._client.send('Input.dispatchMouseEvent', {
|
||||
this.#button = 'none';
|
||||
await this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased',
|
||||
button,
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
x: this.#x,
|
||||
y: this.#y,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
clickCount,
|
||||
});
|
||||
}
|
||||
@ -477,13 +486,13 @@ export class Mouse {
|
||||
*/
|
||||
async wheel(options: MouseWheelOptions = {}): Promise<void> {
|
||||
const { deltaX = 0, deltaY = 0 } = options;
|
||||
await this._client.send('Input.dispatchMouseEvent', {
|
||||
await this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseWheel',
|
||||
x: this._x,
|
||||
y: this._y,
|
||||
x: this.#x,
|
||||
y: this.#y,
|
||||
deltaX,
|
||||
deltaY,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
pointerType: 'mouse',
|
||||
});
|
||||
}
|
||||
@ -495,7 +504,7 @@ export class Mouse {
|
||||
*/
|
||||
async drag(start: Point, target: Point): Promise<Protocol.Input.DragData> {
|
||||
const promise = new Promise<Protocol.Input.DragData>((resolve) => {
|
||||
this._client.once('Input.dragIntercepted', (event) =>
|
||||
this.#client.once('Input.dragIntercepted', (event) =>
|
||||
resolve(event.data)
|
||||
);
|
||||
});
|
||||
@ -511,11 +520,11 @@ export class Mouse {
|
||||
* @param data - drag data containing items and operations mask
|
||||
*/
|
||||
async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void> {
|
||||
await this._client.send('Input.dispatchDragEvent', {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'dragEnter',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
@ -526,11 +535,11 @@ export class Mouse {
|
||||
* @param data - drag data containing items and operations mask
|
||||
*/
|
||||
async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void> {
|
||||
await this._client.send('Input.dispatchDragEvent', {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'dragOver',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
@ -541,11 +550,11 @@ export class Mouse {
|
||||
* @param data - drag data containing items and operations mask
|
||||
*/
|
||||
async drop(target: Point, data: Protocol.Input.DragData): Promise<void> {
|
||||
await this._client.send('Input.dispatchDragEvent', {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'drop',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
@ -580,15 +589,15 @@ export class Mouse {
|
||||
* @public
|
||||
*/
|
||||
export class Touchscreen {
|
||||
private _client: CDPSession;
|
||||
private _keyboard: Keyboard;
|
||||
#client: CDPSession;
|
||||
#keyboard: Keyboard;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession, keyboard: Keyboard) {
|
||||
this._client = client;
|
||||
this._keyboard = keyboard;
|
||||
this.#client = client;
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -598,15 +607,15 @@ export class Touchscreen {
|
||||
*/
|
||||
async tap(x: number, y: number): Promise<void> {
|
||||
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
|
||||
await this._client.send('Input.dispatchTouchEvent', {
|
||||
await this.#client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints,
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
await this._client.send('Input.dispatchTouchEvent', {
|
||||
await this.#client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [],
|
||||
modifiers: this._keyboard._modifiers,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import { Frame, FrameManager } from './FrameManager.js';
|
||||
import { debugError, helper } from './helper.js';
|
||||
import { MouseButton } from './Input.js';
|
||||
import { Page, ScreenshotOptions } from './Page.js';
|
||||
import { getQueryHandlerAndSelector } from './QueryHandler.js';
|
||||
import { _getQueryHandlerAndSelector } from './QueryHandler.js';
|
||||
import { KeyInput } from './USKeyboardLayout.js';
|
||||
|
||||
/**
|
||||
@ -62,7 +62,7 @@ export interface BoundingBox extends Point {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function createJSHandle(
|
||||
export function _createJSHandle(
|
||||
context: ExecutionContext,
|
||||
remoteObject: Protocol.Runtime.RemoteObject
|
||||
): JSHandle {
|
||||
@ -103,22 +103,38 @@ const applyOffsetsToQuad = (quad: Point[], offsetX: number, offsetY: number) =>
|
||||
* @public
|
||||
*/
|
||||
export class JSHandle<HandleObjectType = unknown> {
|
||||
#client: CDPSession;
|
||||
#disposed = false;
|
||||
#context: ExecutionContext;
|
||||
#remoteObject: Protocol.Runtime.RemoteObject;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_context: ExecutionContext;
|
||||
get _client(): CDPSession {
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_client: CDPSession;
|
||||
get _disposed(): boolean {
|
||||
return this.#disposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_remoteObject: Protocol.Runtime.RemoteObject;
|
||||
get _remoteObject(): Protocol.Runtime.RemoteObject {
|
||||
return this.#remoteObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_disposed = false;
|
||||
get _context(): ExecutionContext {
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -128,15 +144,15 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
client: CDPSession,
|
||||
remoteObject: Protocol.Runtime.RemoteObject
|
||||
) {
|
||||
this._context = context;
|
||||
this._client = client;
|
||||
this._remoteObject = remoteObject;
|
||||
this.#context = context;
|
||||
this.#client = client;
|
||||
this.#remoteObject = remoteObject;
|
||||
}
|
||||
|
||||
/** Returns the execution context the handle belongs to.
|
||||
*/
|
||||
executionContext(): ExecutionContext {
|
||||
return this._context;
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,15 +238,15 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
* ```
|
||||
*/
|
||||
async getProperties(): Promise<Map<string, JSHandle>> {
|
||||
assert(this._remoteObject.objectId);
|
||||
const response = await this._client.send('Runtime.getProperties', {
|
||||
objectId: this._remoteObject.objectId,
|
||||
assert(this.#remoteObject.objectId);
|
||||
const response = await this.#client.send('Runtime.getProperties', {
|
||||
objectId: this.#remoteObject.objectId,
|
||||
ownProperties: true,
|
||||
});
|
||||
const result = new Map<string, JSHandle>();
|
||||
for (const property of response.result) {
|
||||
if (!property.enumerable || !property.value) continue;
|
||||
result.set(property.name, createJSHandle(this._context, property.value));
|
||||
result.set(property.name, _createJSHandle(this.#context, property.value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -245,16 +261,16 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
* **NOTE** The method throws if the referenced object is not stringifiable.
|
||||
*/
|
||||
async jsonValue<T = unknown>(): Promise<T> {
|
||||
if (this._remoteObject.objectId) {
|
||||
const response = await this._client.send('Runtime.callFunctionOn', {
|
||||
if (this.#remoteObject.objectId) {
|
||||
const response = await this.#client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: 'function() { return this; }',
|
||||
objectId: this._remoteObject.objectId,
|
||||
objectId: this.#remoteObject.objectId,
|
||||
returnByValue: true,
|
||||
awaitPromise: true,
|
||||
});
|
||||
return helper.valueFromRemoteObject(response.result) as T;
|
||||
}
|
||||
return helper.valueFromRemoteObject(this._remoteObject) as T;
|
||||
return helper.valueFromRemoteObject(this.#remoteObject) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -273,9 +289,9 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
* successfully disposed of.
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
if (this._disposed) return;
|
||||
this._disposed = true;
|
||||
await helper.releaseObject(this._client, this._remoteObject);
|
||||
if (this.#disposed) return;
|
||||
this.#disposed = true;
|
||||
await helper.releaseObject(this.#client, this.#remoteObject);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,11 +300,11 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
* @remarks Useful during debugging.
|
||||
*/
|
||||
toString(): string {
|
||||
if (this._remoteObject.objectId) {
|
||||
const type = this._remoteObject.subtype || this._remoteObject.type;
|
||||
if (this.#remoteObject.objectId) {
|
||||
const type = this.#remoteObject.subtype || this.#remoteObject.type;
|
||||
return 'JSHandle@' + type;
|
||||
}
|
||||
return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
|
||||
return 'JSHandle:' + helper.valueFromRemoteObject(this.#remoteObject);
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,9 +345,9 @@ export class JSHandle<HandleObjectType = unknown> {
|
||||
export class ElementHandle<
|
||||
ElementType extends Element = Element
|
||||
> extends JSHandle<ElementType> {
|
||||
private _frame: Frame;
|
||||
private _page: Page;
|
||||
private _frameManager: FrameManager;
|
||||
#frame: Frame;
|
||||
#page: Page;
|
||||
#frameManager: FrameManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -345,11 +361,9 @@ export class ElementHandle<
|
||||
frameManager: FrameManager
|
||||
) {
|
||||
super(context, client, remoteObject);
|
||||
this._client = client;
|
||||
this._remoteObject = remoteObject;
|
||||
this._frame = frame;
|
||||
this._page = page;
|
||||
this._frameManager = frameManager;
|
||||
this.#frame = frame;
|
||||
this.#page = page;
|
||||
this.#frameManager = frameManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -498,10 +512,10 @@ export class ElementHandle<
|
||||
objectId: this._remoteObject.objectId,
|
||||
});
|
||||
if (typeof nodeInfo.node.frameId !== 'string') return null;
|
||||
return this._frameManager.frame(nodeInfo.node.frameId);
|
||||
return this.#frameManager.frame(nodeInfo.node.frameId);
|
||||
}
|
||||
|
||||
private async _scrollIntoViewIfNeeded(): Promise<void> {
|
||||
async #scrollIntoViewIfNeeded(): Promise<void> {
|
||||
const error = await this.evaluate(
|
||||
async (
|
||||
element: Element,
|
||||
@ -541,13 +555,13 @@ export class ElementHandle<
|
||||
}
|
||||
return false;
|
||||
},
|
||||
this._page.isJavaScriptEnabled()
|
||||
this.#page.isJavaScriptEnabled()
|
||||
);
|
||||
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
private async _getOOPIFOffsets(
|
||||
async #getOOPIFOffsets(
|
||||
frame: Frame
|
||||
): Promise<{ offsetX: number; offsetY: number }> {
|
||||
let offsetX = 0;
|
||||
@ -559,17 +573,19 @@ export class ElementHandle<
|
||||
currentFrame = parent;
|
||||
continue;
|
||||
}
|
||||
const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', {
|
||||
frameId: currentFrame._id,
|
||||
});
|
||||
const result = await parent._client.send('DOM.getBoxModel', {
|
||||
const { backendNodeId } = await parent
|
||||
._client()
|
||||
.send('DOM.getFrameOwner', {
|
||||
frameId: currentFrame._id,
|
||||
});
|
||||
const result = await parent._client().send('DOM.getBoxModel', {
|
||||
backendNodeId: backendNodeId,
|
||||
});
|
||||
if (!result) {
|
||||
break;
|
||||
}
|
||||
const contentBoxQuad = result.model.content;
|
||||
const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0];
|
||||
const topLeftCorner = this.#fromProtocolQuad(contentBoxQuad)[0];
|
||||
offsetX += topLeftCorner!.x;
|
||||
offsetY += topLeftCorner!.y;
|
||||
currentFrame = parent;
|
||||
@ -587,7 +603,7 @@ export class ElementHandle<
|
||||
objectId: this._remoteObject.objectId,
|
||||
})
|
||||
.catch(debugError),
|
||||
this._page.client().send('Page.getLayoutMetrics'),
|
||||
this.#page._client().send('Page.getLayoutMetrics'),
|
||||
]);
|
||||
if (!result || !result.quads.length)
|
||||
throw new Error('Node is either not clickable or not an HTMLElement');
|
||||
@ -595,12 +611,12 @@ export class ElementHandle<
|
||||
// Fallback to `layoutViewport` in case of using Firefox.
|
||||
const { clientWidth, clientHeight } =
|
||||
layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport;
|
||||
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
|
||||
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
|
||||
const quads = result.quads
|
||||
.map((quad) => this._fromProtocolQuad(quad))
|
||||
.map((quad) => this.#fromProtocolQuad(quad))
|
||||
.map((quad) => applyOffsetsToQuad(quad, offsetX, offsetY))
|
||||
.map((quad) =>
|
||||
this._intersectQuadWithViewport(quad, clientWidth, clientHeight)
|
||||
this.#intersectQuadWithViewport(quad, clientWidth, clientHeight)
|
||||
)
|
||||
.filter((quad) => computeQuadArea(quad) > 1);
|
||||
if (!quads.length)
|
||||
@ -641,7 +657,7 @@ export class ElementHandle<
|
||||
};
|
||||
}
|
||||
|
||||
private _getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
|
||||
#getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
|
||||
const params: Protocol.DOM.GetBoxModelRequest = {
|
||||
objectId: this._remoteObject.objectId,
|
||||
};
|
||||
@ -650,7 +666,7 @@ export class ElementHandle<
|
||||
.catch((error) => debugError(error));
|
||||
}
|
||||
|
||||
private _fromProtocolQuad(quad: number[]): Point[] {
|
||||
#fromProtocolQuad(quad: number[]): Point[] {
|
||||
return [
|
||||
{ x: quad[0]!, y: quad[1]! },
|
||||
{ x: quad[2]!, y: quad[3]! },
|
||||
@ -659,7 +675,7 @@ export class ElementHandle<
|
||||
];
|
||||
}
|
||||
|
||||
private _intersectQuadWithViewport(
|
||||
#intersectQuadWithViewport(
|
||||
quad: Point[],
|
||||
width: number,
|
||||
height: number
|
||||
@ -676,9 +692,9 @@ export class ElementHandle<
|
||||
* If the element is detached from DOM, the method throws an error.
|
||||
*/
|
||||
async hover(): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const { x, y } = await this.clickablePoint();
|
||||
await this._page.mouse.move(x, y);
|
||||
await this.#page.mouse.move(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -687,9 +703,9 @@ export class ElementHandle<
|
||||
* If the element is detached from DOM, the method throws an error.
|
||||
*/
|
||||
async click(options: ClickOptions = {}): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const { x, y } = await this.clickablePoint(options.offset);
|
||||
await this._page.mouse.click(x, y, options);
|
||||
await this.#page.mouse.click(x, y, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -697,12 +713,12 @@ export class ElementHandle<
|
||||
*/
|
||||
async drag(target: Point): Promise<Protocol.Input.DragData> {
|
||||
assert(
|
||||
this._page.isDragInterceptionEnabled(),
|
||||
this.#page.isDragInterceptionEnabled(),
|
||||
'Drag Interception is not enabled!'
|
||||
);
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const start = await this.clickablePoint();
|
||||
return await this._page.mouse.drag(start, target);
|
||||
return await this.#page.mouse.drag(start, target);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -711,9 +727,9 @@ export class ElementHandle<
|
||||
async dragEnter(
|
||||
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
|
||||
): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const target = await this.clickablePoint();
|
||||
await this._page.mouse.dragEnter(target, data);
|
||||
await this.#page.mouse.dragEnter(target, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -722,9 +738,9 @@ export class ElementHandle<
|
||||
async dragOver(
|
||||
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
|
||||
): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const target = await this.clickablePoint();
|
||||
await this._page.mouse.dragOver(target, data);
|
||||
await this.#page.mouse.dragOver(target, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -733,9 +749,9 @@ export class ElementHandle<
|
||||
async drop(
|
||||
data: Protocol.Input.DragData = { items: [], dragOperationsMask: 1 }
|
||||
): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const destination = await this.clickablePoint();
|
||||
await this._page.mouse.drop(destination, data);
|
||||
await this.#page.mouse.drop(destination, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -745,10 +761,10 @@ export class ElementHandle<
|
||||
target: ElementHandle,
|
||||
options?: { delay: number }
|
||||
): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const startPoint = await this.clickablePoint();
|
||||
const targetPoint = await target.clickablePoint();
|
||||
await this._page.mouse.dragAndDrop(startPoint, targetPoint, options);
|
||||
await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -883,9 +899,9 @@ export class ElementHandle<
|
||||
* If the element is detached from DOM, the method throws an error.
|
||||
*/
|
||||
async tap(): Promise<void> {
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
const { x, y } = await this.clickablePoint();
|
||||
await this._page.touchscreen.tap(x, y);
|
||||
await this.#page.touchscreen.tap(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -921,7 +937,7 @@ export class ElementHandle<
|
||||
*/
|
||||
async type(text: string, options?: { delay: number }): Promise<void> {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
await this.#page.keyboard.type(text, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -940,7 +956,7 @@ export class ElementHandle<
|
||||
*/
|
||||
async press(key: KeyInput, options?: PressOptions): Promise<void> {
|
||||
await this.focus();
|
||||
await this._page.keyboard.press(key, options);
|
||||
await this.#page.keyboard.press(key, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -948,11 +964,11 @@ export class ElementHandle<
|
||||
* or `null` if the element is not visible.
|
||||
*/
|
||||
async boundingBox(): Promise<BoundingBox | null> {
|
||||
const result = await this._getBoxModel();
|
||||
const result = await this.#getBoxModel();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
|
||||
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
|
||||
const quad = result.model.border;
|
||||
const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!);
|
||||
const y = Math.min(quad[1]!, quad[3]!, quad[5]!, quad[7]!);
|
||||
@ -971,31 +987,31 @@ export class ElementHandle<
|
||||
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
|
||||
*/
|
||||
async boxModel(): Promise<BoxModel | null> {
|
||||
const result = await this._getBoxModel();
|
||||
const result = await this.#getBoxModel();
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
|
||||
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
|
||||
|
||||
const { content, padding, border, margin, width, height } = result.model;
|
||||
return {
|
||||
content: applyOffsetsToQuad(
|
||||
this._fromProtocolQuad(content),
|
||||
this.#fromProtocolQuad(content),
|
||||
offsetX,
|
||||
offsetY
|
||||
),
|
||||
padding: applyOffsetsToQuad(
|
||||
this._fromProtocolQuad(padding),
|
||||
this.#fromProtocolQuad(padding),
|
||||
offsetX,
|
||||
offsetY
|
||||
),
|
||||
border: applyOffsetsToQuad(
|
||||
this._fromProtocolQuad(border),
|
||||
this.#fromProtocolQuad(border),
|
||||
offsetX,
|
||||
offsetY
|
||||
),
|
||||
margin: applyOffsetsToQuad(
|
||||
this._fromProtocolQuad(margin),
|
||||
this.#fromProtocolQuad(margin),
|
||||
offsetX,
|
||||
offsetY
|
||||
),
|
||||
@ -1015,7 +1031,7 @@ export class ElementHandle<
|
||||
let boundingBox = await this.boundingBox();
|
||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
|
||||
const viewport = this._page.viewport();
|
||||
const viewport = this.#page.viewport();
|
||||
assert(viewport);
|
||||
|
||||
if (
|
||||
@ -1026,12 +1042,12 @@ export class ElementHandle<
|
||||
width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
||||
height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
||||
};
|
||||
await this._page.setViewport(Object.assign({}, viewport, newViewport));
|
||||
await this.#page.setViewport(Object.assign({}, viewport, newViewport));
|
||||
|
||||
needsViewportReset = true;
|
||||
}
|
||||
|
||||
await this._scrollIntoViewIfNeeded();
|
||||
await this.#scrollIntoViewIfNeeded();
|
||||
|
||||
boundingBox = await this.boundingBox();
|
||||
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
||||
@ -1047,7 +1063,7 @@ export class ElementHandle<
|
||||
clip.x += pageX;
|
||||
clip.y += pageY;
|
||||
|
||||
const imageData = await this._page.screenshot(
|
||||
const imageData = await this.#page.screenshot(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
@ -1057,7 +1073,7 @@ export class ElementHandle<
|
||||
)
|
||||
);
|
||||
|
||||
if (needsViewportReset) await this._page.setViewport(viewport);
|
||||
if (needsViewportReset) await this.#page.setViewport(viewport);
|
||||
|
||||
return imageData;
|
||||
}
|
||||
@ -1073,7 +1089,7 @@ export class ElementHandle<
|
||||
selector: string
|
||||
): Promise<ElementHandle<T> | null> {
|
||||
const { updatedSelector, queryHandler } =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
_getQueryHandlerAndSelector(selector);
|
||||
assert(
|
||||
queryHandler.queryOne,
|
||||
'Cannot handle queries for a single element with the given selector'
|
||||
@ -1096,7 +1112,7 @@ export class ElementHandle<
|
||||
selector: string
|
||||
): Promise<Array<ElementHandle<T>>> {
|
||||
const { updatedSelector, queryHandler } =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
_getQueryHandlerAndSelector(selector);
|
||||
assert(
|
||||
queryHandler.queryAll,
|
||||
'Cannot handle queries for a multiple element with the given selector'
|
||||
@ -1184,7 +1200,7 @@ export class ElementHandle<
|
||||
...args: SerializableOrJSHandle[]
|
||||
): Promise<WrapElementHandle<ReturnType>> {
|
||||
const { updatedSelector, queryHandler } =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
_getQueryHandlerAndSelector(selector);
|
||||
assert(queryHandler.queryAllArray);
|
||||
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
|
||||
const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>(
|
||||
|
@ -60,41 +60,41 @@ const noop = (): void => {};
|
||||
* @internal
|
||||
*/
|
||||
export class LifecycleWatcher {
|
||||
_expectedLifecycle: ProtocolLifeCycleEvent[];
|
||||
_frameManager: FrameManager;
|
||||
_frame: Frame;
|
||||
_timeout: number;
|
||||
_navigationRequest: HTTPRequest | null = null;
|
||||
_eventListeners: PuppeteerEventListener[];
|
||||
#expectedLifecycle: ProtocolLifeCycleEvent[];
|
||||
#frameManager: FrameManager;
|
||||
#frame: Frame;
|
||||
#timeout: number;
|
||||
#navigationRequest: HTTPRequest | null = null;
|
||||
#eventListeners: PuppeteerEventListener[];
|
||||
|
||||
_sameDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
|
||||
_sameDocumentNavigationPromise = new Promise<Error | undefined>((fulfill) => {
|
||||
this._sameDocumentNavigationCompleteCallback = fulfill;
|
||||
#sameDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
|
||||
#sameDocumentNavigationPromise = new Promise<Error | undefined>((fulfill) => {
|
||||
this.#sameDocumentNavigationCompleteCallback = fulfill;
|
||||
});
|
||||
|
||||
_lifecycleCallback: () => void = noop;
|
||||
_lifecyclePromise: Promise<void> = new Promise((fulfill) => {
|
||||
this._lifecycleCallback = fulfill;
|
||||
#lifecycleCallback: () => void = noop;
|
||||
#lifecyclePromise: Promise<void> = new Promise((fulfill) => {
|
||||
this.#lifecycleCallback = fulfill;
|
||||
});
|
||||
|
||||
_newDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
|
||||
_newDocumentNavigationPromise: Promise<Error | undefined> = new Promise(
|
||||
#newDocumentNavigationCompleteCallback: (x?: Error) => void = noop;
|
||||
#newDocumentNavigationPromise: Promise<Error | undefined> = new Promise(
|
||||
(fulfill) => {
|
||||
this._newDocumentNavigationCompleteCallback = fulfill;
|
||||
this.#newDocumentNavigationCompleteCallback = fulfill;
|
||||
}
|
||||
);
|
||||
|
||||
_terminationCallback: (x?: Error) => void = noop;
|
||||
_terminationPromise: Promise<Error | undefined> = new Promise((fulfill) => {
|
||||
this._terminationCallback = fulfill;
|
||||
#terminationCallback: (x?: Error) => void = noop;
|
||||
#terminationPromise: Promise<Error | undefined> = new Promise((fulfill) => {
|
||||
this.#terminationCallback = fulfill;
|
||||
});
|
||||
|
||||
_timeoutPromise: Promise<TimeoutError | undefined>;
|
||||
#timeoutPromise: Promise<TimeoutError | undefined>;
|
||||
|
||||
_maximumTimer?: NodeJS.Timeout;
|
||||
_hasSameDocumentNavigation?: boolean;
|
||||
_newDocumentNavigation?: boolean;
|
||||
_swapped?: boolean;
|
||||
#maximumTimer?: NodeJS.Timeout;
|
||||
#hasSameDocumentNavigation?: boolean;
|
||||
#newDocumentNavigation?: boolean;
|
||||
#swapped?: boolean;
|
||||
|
||||
constructor(
|
||||
frameManager: FrameManager,
|
||||
@ -104,137 +104,138 @@ export class LifecycleWatcher {
|
||||
) {
|
||||
if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice();
|
||||
else if (typeof waitUntil === 'string') waitUntil = [waitUntil];
|
||||
this._expectedLifecycle = waitUntil.map((value) => {
|
||||
this.#expectedLifecycle = waitUntil.map((value) => {
|
||||
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
|
||||
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
|
||||
return protocolEvent as ProtocolLifeCycleEvent;
|
||||
});
|
||||
|
||||
this._frameManager = frameManager;
|
||||
this._frame = frame;
|
||||
this._timeout = timeout;
|
||||
this._eventListeners = [
|
||||
this.#frameManager = frameManager;
|
||||
this.#frame = frame;
|
||||
this.#timeout = timeout;
|
||||
this.#eventListeners = [
|
||||
helper.addEventListener(
|
||||
frameManager._client,
|
||||
CDPSessionEmittedEvents.Disconnected,
|
||||
this._terminate.bind(
|
||||
this.#terminate.bind(
|
||||
this,
|
||||
new Error('Navigation failed because browser has disconnected!')
|
||||
)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager,
|
||||
this.#frameManager,
|
||||
FrameManagerEmittedEvents.LifecycleEvent,
|
||||
this._checkLifecycleComplete.bind(this)
|
||||
this.#checkLifecycleComplete.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager,
|
||||
this.#frameManager,
|
||||
FrameManagerEmittedEvents.FrameNavigatedWithinDocument,
|
||||
this._navigatedWithinDocument.bind(this)
|
||||
this.#navigatedWithinDocument.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager,
|
||||
this.#frameManager,
|
||||
FrameManagerEmittedEvents.FrameNavigated,
|
||||
this._navigated.bind(this)
|
||||
this.#navigated.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager,
|
||||
this.#frameManager,
|
||||
FrameManagerEmittedEvents.FrameSwapped,
|
||||
this._frameSwapped.bind(this)
|
||||
this.#frameSwapped.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager,
|
||||
this.#frameManager,
|
||||
FrameManagerEmittedEvents.FrameDetached,
|
||||
this._onFrameDetached.bind(this)
|
||||
this.#onFrameDetached.bind(this)
|
||||
),
|
||||
helper.addEventListener(
|
||||
this._frameManager.networkManager(),
|
||||
this.#frameManager.networkManager(),
|
||||
NetworkManagerEmittedEvents.Request,
|
||||
this._onRequest.bind(this)
|
||||
this.#onRequest.bind(this)
|
||||
),
|
||||
];
|
||||
|
||||
this._timeoutPromise = this._createTimeoutPromise();
|
||||
this._checkLifecycleComplete();
|
||||
this.#timeoutPromise = this.#createTimeoutPromise();
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_onRequest(request: HTTPRequest): void {
|
||||
if (request.frame() !== this._frame || !request.isNavigationRequest())
|
||||
#onRequest(request: HTTPRequest): void {
|
||||
if (request.frame() !== this.#frame || !request.isNavigationRequest())
|
||||
return;
|
||||
this._navigationRequest = request;
|
||||
this.#navigationRequest = request;
|
||||
}
|
||||
|
||||
_onFrameDetached(frame: Frame): void {
|
||||
if (this._frame === frame) {
|
||||
this._terminationCallback.call(
|
||||
#onFrameDetached(frame: Frame): void {
|
||||
if (this.#frame === frame) {
|
||||
this.#terminationCallback.call(
|
||||
null,
|
||||
new Error('Navigating frame was detached')
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._checkLifecycleComplete();
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
async navigationResponse(): Promise<HTTPResponse | null> {
|
||||
// We may need to wait for ExtraInfo events before the request is complete.
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
return this.#navigationRequest ? this.#navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
_terminate(error: Error): void {
|
||||
this._terminationCallback.call(null, error);
|
||||
#terminate(error: Error): void {
|
||||
this.#terminationCallback.call(null, error);
|
||||
}
|
||||
|
||||
sameDocumentNavigationPromise(): Promise<Error | undefined> {
|
||||
return this._sameDocumentNavigationPromise;
|
||||
return this.#sameDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
newDocumentNavigationPromise(): Promise<Error | undefined> {
|
||||
return this._newDocumentNavigationPromise;
|
||||
return this.#newDocumentNavigationPromise;
|
||||
}
|
||||
|
||||
lifecyclePromise(): Promise<void> {
|
||||
return this._lifecyclePromise;
|
||||
return this.#lifecyclePromise;
|
||||
}
|
||||
|
||||
timeoutOrTerminationPromise(): Promise<Error | TimeoutError | undefined> {
|
||||
return Promise.race([this._timeoutPromise, this._terminationPromise]);
|
||||
return Promise.race([this.#timeoutPromise, this.#terminationPromise]);
|
||||
}
|
||||
|
||||
_createTimeoutPromise(): Promise<TimeoutError | undefined> {
|
||||
if (!this._timeout) return new Promise(() => {});
|
||||
async #createTimeoutPromise(): Promise<TimeoutError | undefined> {
|
||||
if (!this.#timeout) return new Promise(noop);
|
||||
const errorMessage =
|
||||
'Navigation timeout of ' + this._timeout + ' ms exceeded';
|
||||
return new Promise(
|
||||
(fulfill) => (this._maximumTimer = setTimeout(fulfill, this._timeout))
|
||||
).then(() => new TimeoutError(errorMessage));
|
||||
'Navigation timeout of ' + this.#timeout + ' ms exceeded';
|
||||
await new Promise(
|
||||
(fulfill) => (this.#maximumTimer = setTimeout(fulfill, this.#timeout))
|
||||
);
|
||||
return new TimeoutError(errorMessage);
|
||||
}
|
||||
|
||||
_navigatedWithinDocument(frame: Frame): void {
|
||||
if (frame !== this._frame) return;
|
||||
this._hasSameDocumentNavigation = true;
|
||||
this._checkLifecycleComplete();
|
||||
#navigatedWithinDocument(frame: Frame): void {
|
||||
if (frame !== this.#frame) return;
|
||||
this.#hasSameDocumentNavigation = true;
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_navigated(frame: Frame): void {
|
||||
if (frame !== this._frame) return;
|
||||
this._newDocumentNavigation = true;
|
||||
this._checkLifecycleComplete();
|
||||
#navigated(frame: Frame): void {
|
||||
if (frame !== this.#frame) return;
|
||||
this.#newDocumentNavigation = true;
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_frameSwapped(frame: Frame): void {
|
||||
if (frame !== this._frame) return;
|
||||
this._swapped = true;
|
||||
this._checkLifecycleComplete();
|
||||
#frameSwapped(frame: Frame): void {
|
||||
if (frame !== this.#frame) return;
|
||||
this.#swapped = true;
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
_checkLifecycleComplete(): void {
|
||||
#checkLifecycleComplete(): void {
|
||||
// We expect navigation to commit.
|
||||
if (!checkLifecycle(this._frame, this._expectedLifecycle)) return;
|
||||
this._lifecycleCallback();
|
||||
if (this._hasSameDocumentNavigation)
|
||||
this._sameDocumentNavigationCompleteCallback();
|
||||
if (this._swapped || this._newDocumentNavigation)
|
||||
this._newDocumentNavigationCompleteCallback();
|
||||
if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) return;
|
||||
this.#lifecycleCallback();
|
||||
if (this.#hasSameDocumentNavigation)
|
||||
this.#sameDocumentNavigationCompleteCallback();
|
||||
if (this.#swapped || this.#newDocumentNavigation)
|
||||
this.#newDocumentNavigationCompleteCallback();
|
||||
|
||||
function checkLifecycle(
|
||||
frame: Frame,
|
||||
@ -256,7 +257,7 @@ export class LifecycleWatcher {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this._maximumTimer !== undefined && clearTimeout(this._maximumTimer);
|
||||
helper.removeEventListeners(this.#eventListeners);
|
||||
this.#maximumTimer !== undefined && clearTimeout(this.#maximumTimer);
|
||||
}
|
||||
}
|
||||
|
@ -54,15 +54,15 @@ export class NetworkEventManager {
|
||||
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
|
||||
* (see crbug.com/1196004)
|
||||
*/
|
||||
private _requestWillBeSentMap = new Map<
|
||||
#requestWillBeSentMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Network.RequestWillBeSentEvent
|
||||
>();
|
||||
private _requestPausedMap = new Map<
|
||||
#requestPausedMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Fetch.RequestPausedEvent
|
||||
>();
|
||||
private _httpRequestsMap = new Map<NetworkRequestId, HTTPRequest>();
|
||||
#httpRequestsMap = new Map<NetworkRequestId, HTTPRequest>();
|
||||
|
||||
/*
|
||||
* The below maps are used to reconcile Network.responseReceivedExtraInfo
|
||||
@ -73,40 +73,37 @@ export class NetworkEventManager {
|
||||
* handle redirects, we have to make them Arrays to represent the chain of
|
||||
* events.
|
||||
*/
|
||||
private _responseReceivedExtraInfoMap = new Map<
|
||||
#responseReceivedExtraInfoMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Network.ResponseReceivedExtraInfoEvent[]
|
||||
>();
|
||||
private _queuedRedirectInfoMap = new Map<
|
||||
NetworkRequestId,
|
||||
RedirectInfoList
|
||||
>();
|
||||
private _queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
|
||||
#queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>();
|
||||
#queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
|
||||
|
||||
forget(networkRequestId: NetworkRequestId): void {
|
||||
this._requestWillBeSentMap.delete(networkRequestId);
|
||||
this._requestPausedMap.delete(networkRequestId);
|
||||
this._queuedEventGroupMap.delete(networkRequestId);
|
||||
this._queuedRedirectInfoMap.delete(networkRequestId);
|
||||
this._responseReceivedExtraInfoMap.delete(networkRequestId);
|
||||
this.#requestWillBeSentMap.delete(networkRequestId);
|
||||
this.#requestPausedMap.delete(networkRequestId);
|
||||
this.#queuedEventGroupMap.delete(networkRequestId);
|
||||
this.#queuedRedirectInfoMap.delete(networkRequestId);
|
||||
this.#responseReceivedExtraInfoMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
responseExtraInfo(
|
||||
networkRequestId: NetworkRequestId
|
||||
): Protocol.Network.ResponseReceivedExtraInfoEvent[] {
|
||||
if (!this._responseReceivedExtraInfoMap.has(networkRequestId)) {
|
||||
this._responseReceivedExtraInfoMap.set(networkRequestId, []);
|
||||
if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) {
|
||||
this.#responseReceivedExtraInfoMap.set(networkRequestId, []);
|
||||
}
|
||||
return this._responseReceivedExtraInfoMap.get(
|
||||
return this.#responseReceivedExtraInfoMap.get(
|
||||
networkRequestId
|
||||
) as Protocol.Network.ResponseReceivedExtraInfoEvent[];
|
||||
}
|
||||
|
||||
private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList {
|
||||
if (!this._queuedRedirectInfoMap.has(fetchRequestId)) {
|
||||
this._queuedRedirectInfoMap.set(fetchRequestId, []);
|
||||
if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) {
|
||||
this.#queuedRedirectInfoMap.set(fetchRequestId, []);
|
||||
}
|
||||
return this._queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
|
||||
return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
|
||||
}
|
||||
|
||||
queueRedirectInfo(
|
||||
@ -123,7 +120,7 @@ export class NetworkEventManager {
|
||||
}
|
||||
|
||||
numRequestsInProgress(): number {
|
||||
return [...this._httpRequestsMap].filter(([, request]) => {
|
||||
return [...this.#httpRequestsMap].filter(([, request]) => {
|
||||
return !request.response();
|
||||
}).length;
|
||||
}
|
||||
@ -132,62 +129,62 @@ export class NetworkEventManager {
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: Protocol.Network.RequestWillBeSentEvent
|
||||
): void {
|
||||
this._requestWillBeSentMap.set(networkRequestId, event);
|
||||
this.#requestWillBeSentMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
getRequestWillBeSent(
|
||||
networkRequestId: NetworkRequestId
|
||||
): Protocol.Network.RequestWillBeSentEvent | undefined {
|
||||
return this._requestWillBeSentMap.get(networkRequestId);
|
||||
return this.#requestWillBeSentMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void {
|
||||
this._requestWillBeSentMap.delete(networkRequestId);
|
||||
this.#requestWillBeSentMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
getRequestPaused(
|
||||
networkRequestId: NetworkRequestId
|
||||
): Protocol.Fetch.RequestPausedEvent | undefined {
|
||||
return this._requestPausedMap.get(networkRequestId);
|
||||
return this.#requestPausedMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
forgetRequestPaused(networkRequestId: NetworkRequestId): void {
|
||||
this._requestPausedMap.delete(networkRequestId);
|
||||
this.#requestPausedMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
storeRequestPaused(
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: Protocol.Fetch.RequestPausedEvent
|
||||
): void {
|
||||
this._requestPausedMap.set(networkRequestId, event);
|
||||
this.#requestPausedMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
getRequest(networkRequestId: NetworkRequestId): HTTPRequest | undefined {
|
||||
return this._httpRequestsMap.get(networkRequestId);
|
||||
return this.#httpRequestsMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
storeRequest(networkRequestId: NetworkRequestId, request: HTTPRequest): void {
|
||||
this._httpRequestsMap.set(networkRequestId, request);
|
||||
this.#httpRequestsMap.set(networkRequestId, request);
|
||||
}
|
||||
|
||||
forgetRequest(networkRequestId: NetworkRequestId): void {
|
||||
this._httpRequestsMap.delete(networkRequestId);
|
||||
this.#httpRequestsMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
getQueuedEventGroup(
|
||||
networkRequestId: NetworkRequestId
|
||||
): QueuedEventGroup | undefined {
|
||||
return this._queuedEventGroupMap.get(networkRequestId);
|
||||
return this.#queuedEventGroupMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
queueEventGroup(
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: QueuedEventGroup
|
||||
): void {
|
||||
this._queuedEventGroupMap.set(networkRequestId, event);
|
||||
this.#queuedEventGroupMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void {
|
||||
this._queuedEventGroupMap.delete(networkRequestId);
|
||||
this.#queuedEventGroupMap.delete(networkRequestId);
|
||||
}
|
||||
}
|
||||
|
@ -79,19 +79,17 @@ interface FrameManager {
|
||||
* @internal
|
||||
*/
|
||||
export class NetworkManager extends EventEmitter {
|
||||
_client: CDPSession;
|
||||
_ignoreHTTPSErrors: boolean;
|
||||
_frameManager: FrameManager;
|
||||
|
||||
_networkEventManager = new NetworkEventManager();
|
||||
|
||||
_extraHTTPHeaders: Record<string, string> = {};
|
||||
_credentials?: Credentials;
|
||||
_attemptedAuthentications = new Set<string>();
|
||||
_userRequestInterceptionEnabled = false;
|
||||
_protocolRequestInterceptionEnabled = false;
|
||||
_userCacheDisabled = false;
|
||||
_emulatedNetworkConditions: InternalNetworkConditions = {
|
||||
#client: CDPSession;
|
||||
#ignoreHTTPSErrors: boolean;
|
||||
#frameManager: FrameManager;
|
||||
#networkEventManager = new NetworkEventManager();
|
||||
#extraHTTPHeaders: Record<string, string> = {};
|
||||
#credentials?: Credentials;
|
||||
#attemptedAuthentications = new Set<string>();
|
||||
#userRequestInterceptionEnabled = false;
|
||||
#protocolRequestInterceptionEnabled = false;
|
||||
#userCacheDisabled = false;
|
||||
#emulatedNetworkConditions: InternalNetworkConditions = {
|
||||
offline: false,
|
||||
upload: -1,
|
||||
download: -1,
|
||||
@ -104,100 +102,100 @@ export class NetworkManager extends EventEmitter {
|
||||
frameManager: FrameManager
|
||||
) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._frameManager = frameManager;
|
||||
this.#client = client;
|
||||
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this.#frameManager = frameManager;
|
||||
|
||||
this._client.on('Fetch.requestPaused', this._onRequestPaused.bind(this));
|
||||
this._client.on('Fetch.authRequired', this._onAuthRequired.bind(this));
|
||||
this._client.on(
|
||||
this.#client.on('Fetch.requestPaused', this.#onRequestPaused.bind(this));
|
||||
this.#client.on('Fetch.authRequired', this.#onAuthRequired.bind(this));
|
||||
this.#client.on(
|
||||
'Network.requestWillBeSent',
|
||||
this._onRequestWillBeSent.bind(this)
|
||||
this.#onRequestWillBeSent.bind(this)
|
||||
);
|
||||
this._client.on(
|
||||
this.#client.on(
|
||||
'Network.requestServedFromCache',
|
||||
this._onRequestServedFromCache.bind(this)
|
||||
this.#onRequestServedFromCache.bind(this)
|
||||
);
|
||||
this._client.on(
|
||||
this.#client.on(
|
||||
'Network.responseReceived',
|
||||
this._onResponseReceived.bind(this)
|
||||
this.#onResponseReceived.bind(this)
|
||||
);
|
||||
this._client.on(
|
||||
this.#client.on(
|
||||
'Network.loadingFinished',
|
||||
this._onLoadingFinished.bind(this)
|
||||
this.#onLoadingFinished.bind(this)
|
||||
);
|
||||
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
|
||||
this._client.on(
|
||||
this.#client.on('Network.loadingFailed', this.#onLoadingFailed.bind(this));
|
||||
this.#client.on(
|
||||
'Network.responseReceivedExtraInfo',
|
||||
this._onResponseReceivedExtraInfo.bind(this)
|
||||
this.#onResponseReceivedExtraInfo.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this._client.send('Network.enable');
|
||||
if (this._ignoreHTTPSErrors)
|
||||
await this._client.send('Security.setIgnoreCertificateErrors', {
|
||||
await this.#client.send('Network.enable');
|
||||
if (this.#ignoreHTTPSErrors)
|
||||
await this.#client.send('Security.setIgnoreCertificateErrors', {
|
||||
ignore: true,
|
||||
});
|
||||
}
|
||||
|
||||
async authenticate(credentials?: Credentials): Promise<void> {
|
||||
this._credentials = credentials;
|
||||
await this._updateProtocolRequestInterception();
|
||||
this.#credentials = credentials;
|
||||
await this.#updateProtocolRequestInterception();
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(
|
||||
extraHTTPHeaders: Record<string, string>
|
||||
): Promise<void> {
|
||||
this._extraHTTPHeaders = {};
|
||||
this.#extraHTTPHeaders = {};
|
||||
for (const key of Object.keys(extraHTTPHeaders)) {
|
||||
const value = extraHTTPHeaders[key];
|
||||
assert(
|
||||
helper.isString(value),
|
||||
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`
|
||||
);
|
||||
this._extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
this.#extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
}
|
||||
await this._client.send('Network.setExtraHTTPHeaders', {
|
||||
headers: this._extraHTTPHeaders,
|
||||
await this.#client.send('Network.setExtraHTTPHeaders', {
|
||||
headers: this.#extraHTTPHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
extraHTTPHeaders(): Record<string, string> {
|
||||
return Object.assign({}, this._extraHTTPHeaders);
|
||||
return Object.assign({}, this.#extraHTTPHeaders);
|
||||
}
|
||||
|
||||
numRequestsInProgress(): number {
|
||||
return this._networkEventManager.numRequestsInProgress();
|
||||
return this.#networkEventManager.numRequestsInProgress();
|
||||
}
|
||||
|
||||
async setOfflineMode(value: boolean): Promise<void> {
|
||||
this._emulatedNetworkConditions.offline = value;
|
||||
await this._updateNetworkConditions();
|
||||
this.#emulatedNetworkConditions.offline = value;
|
||||
await this.#updateNetworkConditions();
|
||||
}
|
||||
|
||||
async emulateNetworkConditions(
|
||||
networkConditions: NetworkConditions | null
|
||||
): Promise<void> {
|
||||
this._emulatedNetworkConditions.upload = networkConditions
|
||||
this.#emulatedNetworkConditions.upload = networkConditions
|
||||
? networkConditions.upload
|
||||
: -1;
|
||||
this._emulatedNetworkConditions.download = networkConditions
|
||||
this.#emulatedNetworkConditions.download = networkConditions
|
||||
? networkConditions.download
|
||||
: -1;
|
||||
this._emulatedNetworkConditions.latency = networkConditions
|
||||
this.#emulatedNetworkConditions.latency = networkConditions
|
||||
? networkConditions.latency
|
||||
: 0;
|
||||
|
||||
await this._updateNetworkConditions();
|
||||
await this.#updateNetworkConditions();
|
||||
}
|
||||
|
||||
async _updateNetworkConditions(): Promise<void> {
|
||||
await this._client.send('Network.emulateNetworkConditions', {
|
||||
offline: this._emulatedNetworkConditions.offline,
|
||||
latency: this._emulatedNetworkConditions.latency,
|
||||
uploadThroughput: this._emulatedNetworkConditions.upload,
|
||||
downloadThroughput: this._emulatedNetworkConditions.download,
|
||||
async #updateNetworkConditions(): Promise<void> {
|
||||
await this.#client.send('Network.emulateNetworkConditions', {
|
||||
offline: this.#emulatedNetworkConditions.offline,
|
||||
latency: this.#emulatedNetworkConditions.latency,
|
||||
uploadThroughput: this.#emulatedNetworkConditions.upload,
|
||||
downloadThroughput: this.#emulatedNetworkConditions.download,
|
||||
});
|
||||
}
|
||||
|
||||
@ -205,96 +203,96 @@ export class NetworkManager extends EventEmitter {
|
||||
userAgent: string,
|
||||
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata
|
||||
): Promise<void> {
|
||||
await this._client.send('Network.setUserAgentOverride', {
|
||||
await this.#client.send('Network.setUserAgentOverride', {
|
||||
userAgent: userAgent,
|
||||
userAgentMetadata: userAgentMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
async setCacheEnabled(enabled: boolean): Promise<void> {
|
||||
this._userCacheDisabled = !enabled;
|
||||
await this._updateProtocolCacheDisabled();
|
||||
this.#userCacheDisabled = !enabled;
|
||||
await this.#updateProtocolCacheDisabled();
|
||||
}
|
||||
|
||||
async setRequestInterception(value: boolean): Promise<void> {
|
||||
this._userRequestInterceptionEnabled = value;
|
||||
await this._updateProtocolRequestInterception();
|
||||
this.#userRequestInterceptionEnabled = value;
|
||||
await this.#updateProtocolRequestInterception();
|
||||
}
|
||||
|
||||
async _updateProtocolRequestInterception(): Promise<void> {
|
||||
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
|
||||
if (enabled === this._protocolRequestInterceptionEnabled) return;
|
||||
this._protocolRequestInterceptionEnabled = enabled;
|
||||
async #updateProtocolRequestInterception(): Promise<void> {
|
||||
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
||||
if (enabled === this.#protocolRequestInterceptionEnabled) return;
|
||||
this.#protocolRequestInterceptionEnabled = enabled;
|
||||
if (enabled) {
|
||||
await Promise.all([
|
||||
this._updateProtocolCacheDisabled(),
|
||||
this._client.send('Fetch.enable', {
|
||||
this.#updateProtocolCacheDisabled(),
|
||||
this.#client.send('Fetch.enable', {
|
||||
handleAuthRequests: true,
|
||||
patterns: [{ urlPattern: '*' }],
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
await Promise.all([
|
||||
this._updateProtocolCacheDisabled(),
|
||||
this._client.send('Fetch.disable'),
|
||||
this.#updateProtocolCacheDisabled(),
|
||||
this.#client.send('Fetch.disable'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
_cacheDisabled(): boolean {
|
||||
return this._userCacheDisabled;
|
||||
#cacheDisabled(): boolean {
|
||||
return this.#userCacheDisabled;
|
||||
}
|
||||
|
||||
async _updateProtocolCacheDisabled(): Promise<void> {
|
||||
await this._client.send('Network.setCacheDisabled', {
|
||||
cacheDisabled: this._cacheDisabled(),
|
||||
async #updateProtocolCacheDisabled(): Promise<void> {
|
||||
await this.#client.send('Network.setCacheDisabled', {
|
||||
cacheDisabled: this.#cacheDisabled(),
|
||||
});
|
||||
}
|
||||
|
||||
_onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void {
|
||||
#onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void {
|
||||
// Request interception doesn't happen for data URLs with Network Service.
|
||||
if (
|
||||
this._userRequestInterceptionEnabled &&
|
||||
this.#userRequestInterceptionEnabled &&
|
||||
!event.request.url.startsWith('data:')
|
||||
) {
|
||||
const { requestId: networkRequestId } = event;
|
||||
|
||||
this._networkEventManager.storeRequestWillBeSent(networkRequestId, event);
|
||||
this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
|
||||
|
||||
/**
|
||||
* CDP may have sent a Fetch.requestPaused event already. Check for it.
|
||||
*/
|
||||
const requestPausedEvent =
|
||||
this._networkEventManager.getRequestPaused(networkRequestId);
|
||||
this.#networkEventManager.getRequestPaused(networkRequestId);
|
||||
if (requestPausedEvent) {
|
||||
const { requestId: fetchRequestId } = requestPausedEvent;
|
||||
this._patchRequestEventHeaders(event, requestPausedEvent);
|
||||
this._onRequest(event, fetchRequestId);
|
||||
this._networkEventManager.forgetRequestPaused(networkRequestId);
|
||||
this.#patchRequestEventHeaders(event, requestPausedEvent);
|
||||
this.#onRequest(event, fetchRequestId);
|
||||
this.#networkEventManager.forgetRequestPaused(networkRequestId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
this._onRequest(event, undefined);
|
||||
this.#onRequest(event, undefined);
|
||||
}
|
||||
|
||||
_onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void {
|
||||
#onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void {
|
||||
/* TODO(jacktfranklin): This is defined in protocol.d.ts but not
|
||||
* in an easily referrable way - we should look at exposing it.
|
||||
*/
|
||||
type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials';
|
||||
let response: AuthResponse = 'Default';
|
||||
if (this._attemptedAuthentications.has(event.requestId)) {
|
||||
if (this.#attemptedAuthentications.has(event.requestId)) {
|
||||
response = 'CancelAuth';
|
||||
} else if (this._credentials) {
|
||||
} else if (this.#credentials) {
|
||||
response = 'ProvideCredentials';
|
||||
this._attemptedAuthentications.add(event.requestId);
|
||||
this.#attemptedAuthentications.add(event.requestId);
|
||||
}
|
||||
const { username, password } = this._credentials || {
|
||||
const { username, password } = this.#credentials || {
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
};
|
||||
this._client
|
||||
this.#client
|
||||
.send('Fetch.continueWithAuth', {
|
||||
requestId: event.requestId,
|
||||
authChallengeResponse: { response, username, password },
|
||||
@ -308,15 +306,13 @@ export class NetworkManager extends EventEmitter {
|
||||
*
|
||||
* CDP may send multiple Fetch.requestPaused
|
||||
* for the same Network.requestWillBeSent.
|
||||
*
|
||||
*
|
||||
*/
|
||||
_onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void {
|
||||
#onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void {
|
||||
if (
|
||||
!this._userRequestInterceptionEnabled &&
|
||||
this._protocolRequestInterceptionEnabled
|
||||
!this.#userRequestInterceptionEnabled &&
|
||||
this.#protocolRequestInterceptionEnabled
|
||||
) {
|
||||
this._client
|
||||
this.#client
|
||||
.send('Fetch.continueRequest', {
|
||||
requestId: event.requestId,
|
||||
})
|
||||
@ -331,7 +327,7 @@ export class NetworkManager extends EventEmitter {
|
||||
|
||||
const requestWillBeSentEvent = (() => {
|
||||
const requestWillBeSentEvent =
|
||||
this._networkEventManager.getRequestWillBeSent(networkRequestId);
|
||||
this.#networkEventManager.getRequestWillBeSent(networkRequestId);
|
||||
|
||||
// redirect requests have the same `requestId`,
|
||||
if (
|
||||
@ -339,21 +335,21 @@ export class NetworkManager extends EventEmitter {
|
||||
(requestWillBeSentEvent.request.url !== event.request.url ||
|
||||
requestWillBeSentEvent.request.method !== event.request.method)
|
||||
) {
|
||||
this._networkEventManager.forgetRequestWillBeSent(networkRequestId);
|
||||
this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
|
||||
return;
|
||||
}
|
||||
return requestWillBeSentEvent;
|
||||
})();
|
||||
|
||||
if (requestWillBeSentEvent) {
|
||||
this._patchRequestEventHeaders(requestWillBeSentEvent, event);
|
||||
this._onRequest(requestWillBeSentEvent, fetchRequestId);
|
||||
this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
|
||||
this.#onRequest(requestWillBeSentEvent, fetchRequestId);
|
||||
} else {
|
||||
this._networkEventManager.storeRequestPaused(networkRequestId, event);
|
||||
this.#networkEventManager.storeRequestPaused(networkRequestId, event);
|
||||
}
|
||||
}
|
||||
|
||||
_patchRequestEventHeaders(
|
||||
#patchRequestEventHeaders(
|
||||
requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
|
||||
requestPausedEvent: Protocol.Fetch.RequestPausedEvent
|
||||
): void {
|
||||
@ -364,7 +360,7 @@ export class NetworkManager extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
_onRequest(
|
||||
#onRequest(
|
||||
event: Protocol.Network.RequestWillBeSentEvent,
|
||||
fetchRequestId?: FetchRequestId
|
||||
): void {
|
||||
@ -379,11 +375,11 @@ export class NetworkManager extends EventEmitter {
|
||||
// response/requestfinished.
|
||||
let redirectResponseExtraInfo = null;
|
||||
if (event.redirectHasExtraInfo) {
|
||||
redirectResponseExtraInfo = this._networkEventManager
|
||||
redirectResponseExtraInfo = this.#networkEventManager
|
||||
.responseExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (!redirectResponseExtraInfo) {
|
||||
this._networkEventManager.queueRedirectInfo(event.requestId, {
|
||||
this.#networkEventManager.queueRedirectInfo(event.requestId, {
|
||||
event,
|
||||
fetchRequestId,
|
||||
});
|
||||
@ -391,11 +387,11 @@ export class NetworkManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
const request = this._networkEventManager.getRequest(event.requestId);
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// If we connect late to the target, we could have missed the
|
||||
// requestWillBeSent event.
|
||||
if (request) {
|
||||
this._handleRequestRedirect(
|
||||
this.#handleRequestRedirect(
|
||||
request,
|
||||
event.redirectResponse,
|
||||
redirectResponseExtraInfo
|
||||
@ -404,37 +400,37 @@ export class NetworkManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
const frame = event.frameId
|
||||
? this._frameManager.frame(event.frameId)
|
||||
? this.#frameManager.frame(event.frameId)
|
||||
: null;
|
||||
|
||||
const request = new HTTPRequest(
|
||||
this._client,
|
||||
this.#client,
|
||||
frame,
|
||||
fetchRequestId,
|
||||
this._userRequestInterceptionEnabled,
|
||||
this.#userRequestInterceptionEnabled,
|
||||
event,
|
||||
redirectChain
|
||||
);
|
||||
this._networkEventManager.storeRequest(event.requestId, request);
|
||||
this.#networkEventManager.storeRequest(event.requestId, request);
|
||||
this.emit(NetworkManagerEmittedEvents.Request, request);
|
||||
request.finalizeInterceptions();
|
||||
}
|
||||
|
||||
_onRequestServedFromCache(
|
||||
#onRequestServedFromCache(
|
||||
event: Protocol.Network.RequestServedFromCacheEvent
|
||||
): void {
|
||||
const request = this._networkEventManager.getRequest(event.requestId);
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
if (request) request._fromMemoryCache = true;
|
||||
this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request);
|
||||
}
|
||||
|
||||
_handleRequestRedirect(
|
||||
#handleRequestRedirect(
|
||||
request: HTTPRequest,
|
||||
responsePayload: Protocol.Network.Response,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
|
||||
): void {
|
||||
const response = new HTTPResponse(
|
||||
this._client,
|
||||
this.#client,
|
||||
request,
|
||||
responsePayload,
|
||||
extraInfo
|
||||
@ -444,22 +440,22 @@ export class NetworkManager extends EventEmitter {
|
||||
response._resolveBody(
|
||||
new Error('Response body is unavailable for redirect responses')
|
||||
);
|
||||
this._forgetRequest(request, false);
|
||||
this.#forgetRequest(request, false);
|
||||
this.emit(NetworkManagerEmittedEvents.Response, response);
|
||||
this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
|
||||
}
|
||||
|
||||
_emitResponseEvent(
|
||||
#emitResponseEvent(
|
||||
responseReceived: Protocol.Network.ResponseReceivedEvent,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
|
||||
): void {
|
||||
const request = this._networkEventManager.getRequest(
|
||||
const request = this.#networkEventManager.getRequest(
|
||||
responseReceived.requestId
|
||||
);
|
||||
// FileUpload sends a response without a matching request.
|
||||
if (!request) return;
|
||||
|
||||
const extraInfos = this._networkEventManager.responseExtraInfo(
|
||||
const extraInfos = this.#networkEventManager.responseExtraInfo(
|
||||
responseReceived.requestId
|
||||
);
|
||||
if (extraInfos.length) {
|
||||
@ -472,7 +468,7 @@ export class NetworkManager extends EventEmitter {
|
||||
}
|
||||
|
||||
const response = new HTTPResponse(
|
||||
this._client,
|
||||
this.#client,
|
||||
request,
|
||||
responseReceived.response,
|
||||
extraInfo
|
||||
@ -481,88 +477,88 @@ export class NetworkManager extends EventEmitter {
|
||||
this.emit(NetworkManagerEmittedEvents.Response, response);
|
||||
}
|
||||
|
||||
_onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void {
|
||||
const request = this._networkEventManager.getRequest(event.requestId);
|
||||
#onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
let extraInfo = null;
|
||||
if (request && !request._fromMemoryCache && event.hasExtraInfo) {
|
||||
extraInfo = this._networkEventManager
|
||||
extraInfo = this.#networkEventManager
|
||||
.responseExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (!extraInfo) {
|
||||
// Wait until we get the corresponding ExtraInfo event.
|
||||
this._networkEventManager.queueEventGroup(event.requestId, {
|
||||
this.#networkEventManager.queueEventGroup(event.requestId, {
|
||||
responseReceivedEvent: event,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._emitResponseEvent(event, extraInfo);
|
||||
this.#emitResponseEvent(event, extraInfo);
|
||||
}
|
||||
|
||||
_onResponseReceivedExtraInfo(
|
||||
#onResponseReceivedExtraInfo(
|
||||
event: Protocol.Network.ResponseReceivedExtraInfoEvent
|
||||
): void {
|
||||
// We may have skipped a redirect response/request pair due to waiting for
|
||||
// this ExtraInfo event. If so, continue that work now that we have the
|
||||
// request.
|
||||
const redirectInfo = this._networkEventManager.takeQueuedRedirectInfo(
|
||||
const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
|
||||
event.requestId
|
||||
);
|
||||
if (redirectInfo) {
|
||||
this._networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
this._onRequest(redirectInfo.event, redirectInfo.fetchRequestId);
|
||||
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
this.#onRequest(redirectInfo.event, redirectInfo.fetchRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// We may have skipped response and loading events because we didn't have
|
||||
// this ExtraInfo event yet. If so, emit those events now.
|
||||
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId
|
||||
);
|
||||
if (queuedEvents) {
|
||||
this._networkEventManager.forgetQueuedEventGroup(event.requestId);
|
||||
this._emitResponseEvent(queuedEvents.responseReceivedEvent, event);
|
||||
this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
|
||||
this.#emitResponseEvent(queuedEvents.responseReceivedEvent, event);
|
||||
if (queuedEvents.loadingFinishedEvent) {
|
||||
this._emitLoadingFinished(queuedEvents.loadingFinishedEvent);
|
||||
this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent);
|
||||
}
|
||||
if (queuedEvents.loadingFailedEvent) {
|
||||
this._emitLoadingFailed(queuedEvents.loadingFailedEvent);
|
||||
this.#emitLoadingFailed(queuedEvents.loadingFailedEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until we get another event that can use this ExtraInfo event.
|
||||
this._networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
}
|
||||
|
||||
_forgetRequest(request: HTTPRequest, events: boolean): void {
|
||||
#forgetRequest(request: HTTPRequest, events: boolean): void {
|
||||
const requestId = request._requestId;
|
||||
const interceptionId = request._interceptionId;
|
||||
|
||||
this._networkEventManager.forgetRequest(requestId);
|
||||
this.#networkEventManager.forgetRequest(requestId);
|
||||
interceptionId !== undefined &&
|
||||
this._attemptedAuthentications.delete(interceptionId);
|
||||
this.#attemptedAuthentications.delete(interceptionId);
|
||||
|
||||
if (events) {
|
||||
this._networkEventManager.forget(requestId);
|
||||
this.#networkEventManager.forget(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
_onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
|
||||
#onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
|
||||
// If the response event for this request is still waiting on a
|
||||
// corresponding ExtraInfo event, then wait to emit this event too.
|
||||
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId
|
||||
);
|
||||
if (queuedEvents) {
|
||||
queuedEvents.loadingFinishedEvent = event;
|
||||
} else {
|
||||
this._emitLoadingFinished(event);
|
||||
this.#emitLoadingFinished(event);
|
||||
}
|
||||
}
|
||||
|
||||
_emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
|
||||
const request = this._networkEventManager.getRequest(event.requestId);
|
||||
#emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request) return;
|
||||
@ -570,32 +566,32 @@ export class NetworkManager extends EventEmitter {
|
||||
// Under certain conditions we never get the Network.responseReceived
|
||||
// event from protocol. @see https://crbug.com/883475
|
||||
if (request.response()) request.response()?._resolveBody(null);
|
||||
this._forgetRequest(request, true);
|
||||
this.#forgetRequest(request, true);
|
||||
this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
|
||||
}
|
||||
|
||||
_onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
|
||||
#onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
|
||||
// If the response event for this request is still waiting on a
|
||||
// corresponding ExtraInfo event, then wait to emit this event too.
|
||||
const queuedEvents = this._networkEventManager.getQueuedEventGroup(
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId
|
||||
);
|
||||
if (queuedEvents) {
|
||||
queuedEvents.loadingFailedEvent = event;
|
||||
} else {
|
||||
this._emitLoadingFailed(event);
|
||||
this.#emitLoadingFailed(event);
|
||||
}
|
||||
}
|
||||
|
||||
_emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
|
||||
const request = this._networkEventManager.getRequest(event.requestId);
|
||||
#emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request) return;
|
||||
request._failureText = event.errorText;
|
||||
const response = request.response();
|
||||
if (response) response._resolveBody(null);
|
||||
this._forgetRequest(request, true);
|
||||
this.#forgetRequest(request, true);
|
||||
this.emit(NetworkManagerEmittedEvents.RequestFailed, request);
|
||||
}
|
||||
}
|
||||
|
@ -182,17 +182,19 @@ export interface PaperFormatDimensions {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const paperFormats: Record<LowerCasePaperFormat, PaperFormatDimensions> =
|
||||
{
|
||||
letter: { width: 8.5, height: 11 },
|
||||
legal: { width: 8.5, height: 14 },
|
||||
tabloid: { width: 11, height: 17 },
|
||||
ledger: { width: 17, height: 11 },
|
||||
a0: { width: 33.1, height: 46.8 },
|
||||
a1: { width: 23.4, height: 33.1 },
|
||||
a2: { width: 16.54, height: 23.4 },
|
||||
a3: { width: 11.7, height: 16.54 },
|
||||
a4: { width: 8.27, height: 11.7 },
|
||||
a5: { width: 5.83, height: 8.27 },
|
||||
a6: { width: 4.13, height: 5.83 },
|
||||
} as const;
|
||||
export const _paperFormats: Record<
|
||||
LowerCasePaperFormat,
|
||||
PaperFormatDimensions
|
||||
> = {
|
||||
letter: { width: 8.5, height: 11 },
|
||||
legal: { width: 8.5, height: 14 },
|
||||
tabloid: { width: 11, height: 17 },
|
||||
ledger: { width: 17, height: 11 },
|
||||
a0: { width: 33.1, height: 46.8 },
|
||||
a1: { width: 23.4, height: 33.1 },
|
||||
a2: { width: 16.54, height: 23.4 },
|
||||
a3: { width: 11.7, height: 16.54 },
|
||||
a4: { width: 8.27, height: 11.7 },
|
||||
a5: { width: 5.83, height: 8.27 },
|
||||
a6: { width: 4.13, height: 5.83 },
|
||||
} as const;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,17 +15,20 @@
|
||||
*/
|
||||
import { puppeteerErrors, PuppeteerErrors } from './Errors.js';
|
||||
import { ConnectionTransport } from './ConnectionTransport.js';
|
||||
import { devicesMap, DevicesMap } from './DeviceDescriptors.js';
|
||||
import { _devicesMap, DevicesMap } from './DeviceDescriptors.js';
|
||||
import { Browser } from './Browser.js';
|
||||
import {
|
||||
registerCustomQueryHandler,
|
||||
unregisterCustomQueryHandler,
|
||||
customQueryHandlerNames,
|
||||
clearCustomQueryHandlers,
|
||||
_registerCustomQueryHandler,
|
||||
_unregisterCustomQueryHandler,
|
||||
_customQueryHandlerNames,
|
||||
_clearCustomQueryHandlers,
|
||||
CustomQueryHandler,
|
||||
} from './QueryHandler.js';
|
||||
import { Product } from './Product.js';
|
||||
import { connectToBrowser, BrowserConnectOptions } from './BrowserConnector.js';
|
||||
import {
|
||||
_connectToBrowser,
|
||||
BrowserConnectOptions,
|
||||
} from './BrowserConnector.js';
|
||||
import {
|
||||
PredefinedNetworkConditions,
|
||||
networkConditions,
|
||||
@ -33,6 +36,7 @@ import {
|
||||
|
||||
/**
|
||||
* Settings that are common to the Puppeteer class, regardless of environment.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface CommonPuppeteerSettings {
|
||||
@ -85,7 +89,7 @@ export class Puppeteer {
|
||||
* @returns Promise which resolves to browser instance.
|
||||
*/
|
||||
connect(options: ConnectOptions): Promise<Browser> {
|
||||
return connectToBrowser(options);
|
||||
return _connectToBrowser(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,7 +114,7 @@ export class Puppeteer {
|
||||
*
|
||||
*/
|
||||
get devices(): DevicesMap {
|
||||
return devicesMap;
|
||||
return _devicesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,27 +186,27 @@ export class Puppeteer {
|
||||
name: string,
|
||||
queryHandler: CustomQueryHandler
|
||||
): void {
|
||||
registerCustomQueryHandler(name, queryHandler);
|
||||
_registerCustomQueryHandler(name, queryHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - The name of the query handler to unregistered.
|
||||
*/
|
||||
unregisterCustomQueryHandler(name: string): void {
|
||||
unregisterCustomQueryHandler(name);
|
||||
_unregisterCustomQueryHandler(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a list with the names of all registered custom query handlers.
|
||||
*/
|
||||
customQueryHandlerNames(): string[] {
|
||||
return customQueryHandlerNames();
|
||||
return _customQueryHandlerNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all registered handlers.
|
||||
*/
|
||||
clearCustomQueryHandlers(): void {
|
||||
clearCustomQueryHandlers();
|
||||
_clearCustomQueryHandlers();
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { WaitForSelectorOptions, DOMWorld } from './DOMWorld.js';
|
||||
import { ElementHandle, JSHandle } from './JSHandle.js';
|
||||
import { ariaHandler } from './AriaQueryHandler.js';
|
||||
import { _ariaHandler } from './AriaQueryHandler.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -76,7 +76,7 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
|
||||
domWorld: DOMWorld,
|
||||
selector: string,
|
||||
options: WaitForSelectorOptions
|
||||
) => domWorld.waitForSelectorInPage(queryOne, selector, options);
|
||||
) => domWorld._waitForSelectorInPage(queryOne, selector, options);
|
||||
}
|
||||
|
||||
if (handler.queryAll) {
|
||||
@ -161,20 +161,20 @@ const pierceHandler = makeQueryHandler({
|
||||
},
|
||||
});
|
||||
|
||||
const _builtInHandlers = new Map([
|
||||
['aria', ariaHandler],
|
||||
const builtInHandlers = new Map([
|
||||
['aria', _ariaHandler],
|
||||
['pierce', pierceHandler],
|
||||
]);
|
||||
const _queryHandlers = new Map(_builtInHandlers);
|
||||
const queryHandlers = new Map(builtInHandlers);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function registerCustomQueryHandler(
|
||||
export function _registerCustomQueryHandler(
|
||||
name: string,
|
||||
handler: CustomQueryHandler
|
||||
): void {
|
||||
if (_queryHandlers.get(name))
|
||||
if (queryHandlers.get(name))
|
||||
throw new Error(`A custom query handler named "${name}" already exists`);
|
||||
|
||||
const isValidName = /^[a-zA-Z]+$/.test(name);
|
||||
@ -183,38 +183,36 @@ export function registerCustomQueryHandler(
|
||||
|
||||
const internalHandler = makeQueryHandler(handler);
|
||||
|
||||
_queryHandlers.set(name, internalHandler);
|
||||
queryHandlers.set(name, internalHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function unregisterCustomQueryHandler(name: string): void {
|
||||
if (_queryHandlers.has(name) && !_builtInHandlers.has(name)) {
|
||||
_queryHandlers.delete(name);
|
||||
export function _unregisterCustomQueryHandler(name: string): void {
|
||||
if (queryHandlers.has(name) && !builtInHandlers.has(name)) {
|
||||
queryHandlers.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function customQueryHandlerNames(): string[] {
|
||||
return [..._queryHandlers.keys()].filter(
|
||||
(name) => !_builtInHandlers.has(name)
|
||||
);
|
||||
export function _customQueryHandlerNames(): string[] {
|
||||
return [...queryHandlers.keys()].filter((name) => !builtInHandlers.has(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function clearCustomQueryHandlers(): void {
|
||||
customQueryHandlerNames().forEach(unregisterCustomQueryHandler);
|
||||
export function _clearCustomQueryHandlers(): void {
|
||||
_customQueryHandlerNames().forEach(_unregisterCustomQueryHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getQueryHandlerAndSelector(selector: string): {
|
||||
export function _getQueryHandlerAndSelector(selector: string): {
|
||||
updatedSelector: string;
|
||||
queryHandler: InternalQueryHandler;
|
||||
} {
|
||||
@ -225,7 +223,7 @@ export function getQueryHandlerAndSelector(selector: string): {
|
||||
const index = selector.indexOf('/');
|
||||
const name = selector.slice(0, index);
|
||||
const updatedSelector = selector.slice(index + 1);
|
||||
const queryHandler = _queryHandlers.get(name);
|
||||
const queryHandler = queryHandlers.get(name);
|
||||
if (!queryHandler)
|
||||
throw new Error(
|
||||
`Query set to use "${name}", but no query handler of that name was found`
|
||||
|
@ -23,30 +23,30 @@ import { Protocol } from 'devtools-protocol';
|
||||
* @public
|
||||
*/
|
||||
export class SecurityDetails {
|
||||
private _subjectName: string;
|
||||
private _issuer: string;
|
||||
private _validFrom: number;
|
||||
private _validTo: number;
|
||||
private _protocol: string;
|
||||
private _sanList: string[];
|
||||
#subjectName: string;
|
||||
#issuer: string;
|
||||
#validFrom: number;
|
||||
#validTo: number;
|
||||
#protocol: string;
|
||||
#sanList: string[];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(securityPayload: Protocol.Network.SecurityDetails) {
|
||||
this._subjectName = securityPayload.subjectName;
|
||||
this._issuer = securityPayload.issuer;
|
||||
this._validFrom = securityPayload.validFrom;
|
||||
this._validTo = securityPayload.validTo;
|
||||
this._protocol = securityPayload.protocol;
|
||||
this._sanList = securityPayload.sanList;
|
||||
this.#subjectName = securityPayload.subjectName;
|
||||
this.#issuer = securityPayload.issuer;
|
||||
this.#validFrom = securityPayload.validFrom;
|
||||
this.#validTo = securityPayload.validTo;
|
||||
this.#protocol = securityPayload.protocol;
|
||||
this.#sanList = securityPayload.sanList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The name of the issuer of the certificate.
|
||||
*/
|
||||
issuer(): string {
|
||||
return this._issuer;
|
||||
return this.#issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,7 +54,7 @@ export class SecurityDetails {
|
||||
* marking the start of the certificate's validity.
|
||||
*/
|
||||
validFrom(): number {
|
||||
return this._validFrom;
|
||||
return this.#validFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,27 +62,27 @@ export class SecurityDetails {
|
||||
* marking the end of the certificate's validity.
|
||||
*/
|
||||
validTo(): number {
|
||||
return this._validTo;
|
||||
return this.#validTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The security protocol being used, e.g. "TLS 1.2".
|
||||
*/
|
||||
protocol(): string {
|
||||
return this._protocol;
|
||||
return this.#protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The name of the subject to which the certificate was issued.
|
||||
*/
|
||||
subjectName(): string {
|
||||
return this._subjectName;
|
||||
return this.#subjectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate.
|
||||
*/
|
||||
subjectAlternativeNames(): string[] {
|
||||
return this._sanList;
|
||||
return this.#sanList;
|
||||
}
|
||||
}
|
||||
|
@ -26,15 +26,15 @@ import { TaskQueue } from './TaskQueue.js';
|
||||
* @public
|
||||
*/
|
||||
export class Target {
|
||||
private _targetInfo: Protocol.Target.TargetInfo;
|
||||
private _browserContext: BrowserContext;
|
||||
#browserContext: BrowserContext;
|
||||
#targetInfo: Protocol.Target.TargetInfo;
|
||||
#sessionFactory: () => Promise<CDPSession>;
|
||||
#ignoreHTTPSErrors: boolean;
|
||||
#defaultViewport?: Viewport;
|
||||
#pagePromise?: Promise<Page>;
|
||||
#workerPromise?: Promise<WebWorker>;
|
||||
#screenshotTaskQueue: TaskQueue;
|
||||
|
||||
private _sessionFactory: () => Promise<CDPSession>;
|
||||
private _ignoreHTTPSErrors: boolean;
|
||||
private _defaultViewport?: Viewport;
|
||||
private _pagePromise?: Promise<Page>;
|
||||
private _workerPromise?: Promise<WebWorker>;
|
||||
private _screenshotTaskQueue: TaskQueue;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -76,22 +76,22 @@ export class Target {
|
||||
screenshotTaskQueue: TaskQueue,
|
||||
isPageTargetCallback: IsPageTargetCallback
|
||||
) {
|
||||
this._targetInfo = targetInfo;
|
||||
this._browserContext = browserContext;
|
||||
this.#targetInfo = targetInfo;
|
||||
this.#browserContext = browserContext;
|
||||
this._targetId = targetInfo.targetId;
|
||||
this._sessionFactory = sessionFactory;
|
||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this._defaultViewport = defaultViewport ?? undefined;
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
this.#sessionFactory = sessionFactory;
|
||||
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this.#defaultViewport = defaultViewport ?? undefined;
|
||||
this.#screenshotTaskQueue = screenshotTaskQueue;
|
||||
this._isPageTargetCallback = isPageTargetCallback;
|
||||
this._initializedPromise = new Promise<boolean>(
|
||||
(fulfill) => (this._initializedCallback = fulfill)
|
||||
).then(async (success) => {
|
||||
if (!success) return false;
|
||||
const opener = this.opener();
|
||||
if (!opener || !opener._pagePromise || this.type() !== 'page')
|
||||
if (!opener || !opener.#pagePromise || this.type() !== 'page')
|
||||
return true;
|
||||
const openerPage = await opener._pagePromise;
|
||||
const openerPage = await opener.#pagePromise;
|
||||
if (!openerPage.listenerCount(PageEmittedEvents.Popup)) return true;
|
||||
const popupPage = await this.page();
|
||||
openerPage.emit(PageEmittedEvents.Popup, popupPage);
|
||||
@ -101,8 +101,8 @@ export class Target {
|
||||
(fulfill) => (this._closedCallback = fulfill)
|
||||
);
|
||||
this._isInitialized =
|
||||
!this._isPageTargetCallback(this._targetInfo) ||
|
||||
this._targetInfo.url !== '';
|
||||
!this._isPageTargetCallback(this.#targetInfo) ||
|
||||
this.#targetInfo.url !== '';
|
||||
if (this._isInitialized) this._initializedCallback(true);
|
||||
}
|
||||
|
||||
@ -110,32 +110,32 @@ export class Target {
|
||||
* Creates a Chrome Devtools Protocol session attached to the target.
|
||||
*/
|
||||
createCDPSession(): Promise<CDPSession> {
|
||||
return this._sessionFactory();
|
||||
return this.#sessionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getTargetInfo(): Protocol.Target.TargetInfo {
|
||||
return this._targetInfo;
|
||||
return this.#targetInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the target is not of type `"page"` or `"background_page"`, returns `null`.
|
||||
*/
|
||||
async page(): Promise<Page | null> {
|
||||
if (this._isPageTargetCallback(this._targetInfo) && !this._pagePromise) {
|
||||
this._pagePromise = this._sessionFactory().then((client) =>
|
||||
Page.create(
|
||||
if (this._isPageTargetCallback(this.#targetInfo) && !this.#pagePromise) {
|
||||
this.#pagePromise = this.#sessionFactory().then((client) =>
|
||||
Page._create(
|
||||
client,
|
||||
this,
|
||||
this._ignoreHTTPSErrors,
|
||||
this._defaultViewport ?? null,
|
||||
this._screenshotTaskQueue
|
||||
this.#ignoreHTTPSErrors,
|
||||
this.#defaultViewport ?? null,
|
||||
this.#screenshotTaskQueue
|
||||
)
|
||||
);
|
||||
}
|
||||
return (await this._pagePromise) ?? null;
|
||||
return (await this.#pagePromise) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,27 +143,27 @@ export class Target {
|
||||
*/
|
||||
async worker(): Promise<WebWorker | null> {
|
||||
if (
|
||||
this._targetInfo.type !== 'service_worker' &&
|
||||
this._targetInfo.type !== 'shared_worker'
|
||||
this.#targetInfo.type !== 'service_worker' &&
|
||||
this.#targetInfo.type !== 'shared_worker'
|
||||
)
|
||||
return null;
|
||||
if (!this._workerPromise) {
|
||||
if (!this.#workerPromise) {
|
||||
// TODO(einbinder): Make workers send their console logs.
|
||||
this._workerPromise = this._sessionFactory().then(
|
||||
this.#workerPromise = this.#sessionFactory().then(
|
||||
(client) =>
|
||||
new WebWorker(
|
||||
client,
|
||||
this._targetInfo.url,
|
||||
this.#targetInfo.url,
|
||||
() => {} /* consoleAPICalled */,
|
||||
() => {} /* exceptionThrown */
|
||||
)
|
||||
);
|
||||
}
|
||||
return this._workerPromise;
|
||||
return this.#workerPromise;
|
||||
}
|
||||
|
||||
url(): string {
|
||||
return this._targetInfo.url;
|
||||
return this.#targetInfo.url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -181,7 +181,7 @@ export class Target {
|
||||
| 'other'
|
||||
| 'browser'
|
||||
| 'webview' {
|
||||
const type = this._targetInfo.type;
|
||||
const type = this.#targetInfo.type;
|
||||
if (
|
||||
type === 'page' ||
|
||||
type === 'background_page' ||
|
||||
@ -198,21 +198,21 @@ export class Target {
|
||||
* Get the browser the target belongs to.
|
||||
*/
|
||||
browser(): Browser {
|
||||
return this._browserContext.browser();
|
||||
return this.#browserContext.browser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the browser context the target belongs to.
|
||||
*/
|
||||
browserContext(): BrowserContext {
|
||||
return this._browserContext;
|
||||
return this.#browserContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target that opened this target. Top-level targets return `null`.
|
||||
*/
|
||||
opener(): Target | undefined {
|
||||
const { openerId } = this._targetInfo;
|
||||
const { openerId } = this.#targetInfo;
|
||||
if (!openerId) return;
|
||||
return this.browser()._targets.get(openerId);
|
||||
}
|
||||
@ -221,12 +221,12 @@ export class Target {
|
||||
* @internal
|
||||
*/
|
||||
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
|
||||
this._targetInfo = targetInfo;
|
||||
this.#targetInfo = targetInfo;
|
||||
|
||||
if (
|
||||
!this._isInitialized &&
|
||||
(!this._isPageTargetCallback(this._targetInfo) ||
|
||||
this._targetInfo.url !== '')
|
||||
(!this._isPageTargetCallback(this.#targetInfo) ||
|
||||
this.#targetInfo.url !== '')
|
||||
) {
|
||||
this._isInitialized = true;
|
||||
this._initializedCallback(true);
|
||||
|
@ -15,15 +15,15 @@
|
||||
*/
|
||||
|
||||
export class TaskQueue {
|
||||
private _chain: Promise<void>;
|
||||
#chain: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this._chain = Promise.resolve();
|
||||
this.#chain = Promise.resolve();
|
||||
}
|
||||
|
||||
postTask<T>(task: () => Promise<T>): Promise<T> {
|
||||
const result = this._chain.then(task);
|
||||
this._chain = result.then(
|
||||
const result = this.#chain.then(task);
|
||||
this.#chain = result.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
|
@ -20,31 +20,31 @@ const DEFAULT_TIMEOUT = 30000;
|
||||
* @internal
|
||||
*/
|
||||
export class TimeoutSettings {
|
||||
_defaultTimeout: number | null;
|
||||
_defaultNavigationTimeout: number | null;
|
||||
#defaultTimeout: number | null;
|
||||
#defaultNavigationTimeout: number | null;
|
||||
|
||||
constructor() {
|
||||
this._defaultTimeout = null;
|
||||
this._defaultNavigationTimeout = null;
|
||||
this.#defaultTimeout = null;
|
||||
this.#defaultNavigationTimeout = null;
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number): void {
|
||||
this._defaultTimeout = timeout;
|
||||
this.#defaultTimeout = timeout;
|
||||
}
|
||||
|
||||
setDefaultNavigationTimeout(timeout: number): void {
|
||||
this._defaultNavigationTimeout = timeout;
|
||||
this.#defaultNavigationTimeout = timeout;
|
||||
}
|
||||
|
||||
navigationTimeout(): number {
|
||||
if (this._defaultNavigationTimeout !== null)
|
||||
return this._defaultNavigationTimeout;
|
||||
if (this._defaultTimeout !== null) return this._defaultTimeout;
|
||||
if (this.#defaultNavigationTimeout !== null)
|
||||
return this.#defaultNavigationTimeout;
|
||||
if (this.#defaultTimeout !== null) return this.#defaultTimeout;
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
timeout(): number {
|
||||
if (this._defaultTimeout !== null) return this._defaultTimeout;
|
||||
if (this.#defaultTimeout !== null) return this.#defaultTimeout;
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
@ -42,26 +42,27 @@ export interface TracingOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Tracing {
|
||||
_client: CDPSession;
|
||||
_recording = false;
|
||||
_path?: string;
|
||||
#client: CDPSession;
|
||||
#recording = false;
|
||||
#path?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this._client = client;
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a trace for the current page.
|
||||
* @remarks
|
||||
* Only one trace can be active at a time per browser.
|
||||
*
|
||||
* @param options - Optional `TracingOptions`.
|
||||
*/
|
||||
async start(options: TracingOptions = {}): Promise<void> {
|
||||
assert(
|
||||
!this._recording,
|
||||
!this.#recording,
|
||||
'Cannot start recording trace while already recording trace.'
|
||||
);
|
||||
|
||||
@ -91,9 +92,9 @@ export class Tracing {
|
||||
.map((cat) => cat.slice(1));
|
||||
const includedCategories = categories.filter((cat) => !cat.startsWith('-'));
|
||||
|
||||
this._path = path;
|
||||
this._recording = true;
|
||||
await this._client.send('Tracing.start', {
|
||||
this.#path = path;
|
||||
this.#recording = true;
|
||||
await this.#client.send('Tracing.start', {
|
||||
transferMode: 'ReturnAsStream',
|
||||
traceConfig: {
|
||||
excludedCategories,
|
||||
@ -113,13 +114,13 @@ export class Tracing {
|
||||
resolve = x;
|
||||
reject = y;
|
||||
});
|
||||
this._client.once('Tracing.tracingComplete', async (event) => {
|
||||
this.#client.once('Tracing.tracingComplete', async (event) => {
|
||||
try {
|
||||
const readable = await helper.getReadableFromProtocolStream(
|
||||
this._client,
|
||||
this.#client,
|
||||
event.stream
|
||||
);
|
||||
const buffer = await helper.getReadableAsBuffer(readable, this._path);
|
||||
const buffer = await helper.getReadableAsBuffer(readable, this.#path);
|
||||
resolve(buffer ?? undefined);
|
||||
} catch (error) {
|
||||
if (isErrorLike(error)) {
|
||||
@ -129,8 +130,8 @@ export class Tracing {
|
||||
}
|
||||
}
|
||||
});
|
||||
await this._client.send('Tracing.end');
|
||||
this._recording = false;
|
||||
await this.#client.send('Tracing.end');
|
||||
this.#recording = false;
|
||||
return contentPromise;
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +294,7 @@ export type KeyInput =
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
|
||||
export const _keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
|
||||
'0': { keyCode: 48, key: '0', code: 'Digit0' },
|
||||
'1': { keyCode: 49, key: '1', code: 'Digit1' },
|
||||
'2': { keyCode: 50, key: '2', code: 'Digit2' },
|
||||
|
@ -61,10 +61,10 @@ type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
|
||||
* @public
|
||||
*/
|
||||
export class WebWorker extends EventEmitter {
|
||||
_client: CDPSession;
|
||||
_url: string;
|
||||
_executionContextPromise: Promise<ExecutionContext>;
|
||||
_executionContextCallback!: (value: ExecutionContext) => void;
|
||||
#client: CDPSession;
|
||||
#url: string;
|
||||
#executionContextPromise: Promise<ExecutionContext>;
|
||||
#executionContextCallback!: (value: ExecutionContext) => void;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -77,31 +77,31 @@ export class WebWorker extends EventEmitter {
|
||||
exceptionThrown: ExceptionThrownCallback
|
||||
) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._url = url;
|
||||
this._executionContextPromise = new Promise<ExecutionContext>(
|
||||
(x) => (this._executionContextCallback = x)
|
||||
this.#client = client;
|
||||
this.#url = url;
|
||||
this.#executionContextPromise = new Promise<ExecutionContext>(
|
||||
(x) => (this.#executionContextCallback = x)
|
||||
);
|
||||
|
||||
let jsHandleFactory: JSHandleFactory;
|
||||
this._client.once('Runtime.executionContextCreated', async (event) => {
|
||||
this.#client.once('Runtime.executionContextCreated', async (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
jsHandleFactory = (remoteObject) =>
|
||||
new JSHandle(executionContext, client, remoteObject);
|
||||
const executionContext = new ExecutionContext(client, event.context);
|
||||
this._executionContextCallback(executionContext);
|
||||
this.#executionContextCallback(executionContext);
|
||||
});
|
||||
|
||||
// This might fail if the target is closed before we receive all execution contexts.
|
||||
this._client.send('Runtime.enable').catch(debugError);
|
||||
this._client.on('Runtime.consoleAPICalled', (event) =>
|
||||
this.#client.send('Runtime.enable').catch(debugError);
|
||||
this.#client.on('Runtime.consoleAPICalled', (event) =>
|
||||
consoleAPICalled(
|
||||
event.type,
|
||||
event.args.map(jsHandleFactory),
|
||||
event.stackTrace
|
||||
)
|
||||
);
|
||||
this._client.on('Runtime.exceptionThrown', (exception) =>
|
||||
this.#client.on('Runtime.exceptionThrown', (exception) =>
|
||||
exceptionThrown(exception.exceptionDetails)
|
||||
);
|
||||
}
|
||||
@ -110,7 +110,7 @@ export class WebWorker extends EventEmitter {
|
||||
* @returns The URL of this web worker.
|
||||
*/
|
||||
url(): string {
|
||||
return this._url;
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,7 +118,7 @@ export class WebWorker extends EventEmitter {
|
||||
* @returns The ExecutionContext the web worker runs in.
|
||||
*/
|
||||
async executionContext(): Promise<ExecutionContext> {
|
||||
return this._executionContextPromise;
|
||||
return this.#executionContextPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,7 +139,7 @@ export class WebWorker extends EventEmitter {
|
||||
pageFunction: Function | string,
|
||||
...args: any[]
|
||||
): Promise<ReturnType> {
|
||||
return (await this._executionContextPromise).evaluate<ReturnType>(
|
||||
return (await this.#executionContextPromise).evaluate<ReturnType>(
|
||||
pageFunction,
|
||||
...args
|
||||
);
|
||||
@ -161,7 +161,7 @@ export class WebWorker extends EventEmitter {
|
||||
pageFunction: EvaluateHandleFn,
|
||||
...args: SerializableOrJSHandle[]
|
||||
): Promise<JSHandle> {
|
||||
return (await this._executionContextPromise).evaluateHandle<HandlerType>(
|
||||
return (await this.#executionContextPromise).evaluateHandle<HandlerType>(
|
||||
pageFunction,
|
||||
...args
|
||||
);
|
||||
|
@ -103,7 +103,7 @@ function archiveName(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function downloadURL(
|
||||
function _downloadURL(
|
||||
product: Product,
|
||||
platform: Platform,
|
||||
host: string,
|
||||
@ -118,26 +118,24 @@ function downloadURL(
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function handleArm64(): void {
|
||||
fs.stat('/usr/bin/chromium-browser', function (_err, stats) {
|
||||
if (stats === undefined) {
|
||||
fs.stat('/usr/bin/chromium', function (_err, stats) {
|
||||
if (stats === undefined) {
|
||||
console.error(
|
||||
'The chromium binary is not available for arm64.' +
|
||||
'\nIf you are on Ubuntu, you can install with: ' +
|
||||
'\n\n sudo apt install chromium\n' +
|
||||
'\n\n sudo apt install chromium-browser\n'
|
||||
);
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let exists = fs.existsSync('/usr/bin/chromium-browser');
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
exists = fs.existsSync('/usr/bin/chromium');
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
'The chromium binary is not available for arm64.' +
|
||||
'\nIf you are on Ubuntu, you can install with: ' +
|
||||
'\n\n sudo apt install chromium\n' +
|
||||
'\n\n sudo apt install chromium-browser\n'
|
||||
);
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const readdirAsync = promisify(fs.readdir.bind(fs));
|
||||
const mkdirAsync = promisify(fs.mkdir.bind(fs));
|
||||
const unlinkAsync = promisify(fs.unlink.bind(fs));
|
||||
@ -195,49 +193,49 @@ export interface BrowserFetcherRevisionInfo {
|
||||
*/
|
||||
|
||||
export class BrowserFetcher {
|
||||
private _product: Product;
|
||||
private _downloadsFolder: string;
|
||||
private _downloadHost: string;
|
||||
private _platform: Platform;
|
||||
#product: Product;
|
||||
#downloadsFolder: string;
|
||||
#downloadHost: string;
|
||||
#platform: Platform;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(projectRoot: string, options: BrowserFetcherOptions = {}) {
|
||||
this._product = (options.product || 'chrome').toLowerCase() as Product;
|
||||
this.#product = (options.product || 'chrome').toLowerCase() as Product;
|
||||
assert(
|
||||
this._product === 'chrome' || this._product === 'firefox',
|
||||
this.#product === 'chrome' || this.#product === 'firefox',
|
||||
`Unknown product: "${options.product}"`
|
||||
);
|
||||
|
||||
this._downloadsFolder =
|
||||
this.#downloadsFolder =
|
||||
options.path ||
|
||||
path.join(projectRoot, browserConfig[this._product].destination);
|
||||
this._downloadHost = options.host || browserConfig[this._product].host;
|
||||
path.join(projectRoot, browserConfig[this.#product].destination);
|
||||
this.#downloadHost = options.host || browserConfig[this.#product].host;
|
||||
|
||||
if (options.platform) {
|
||||
this._platform = options.platform;
|
||||
this.#platform = options.platform;
|
||||
} else {
|
||||
const platform = os.platform();
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
switch (this._product) {
|
||||
switch (this.#product) {
|
||||
case 'chrome':
|
||||
this._platform =
|
||||
this.#platform =
|
||||
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
|
||||
? 'mac_arm'
|
||||
: 'mac';
|
||||
break;
|
||||
case 'firefox':
|
||||
this._platform = 'mac';
|
||||
this.#platform = 'mac';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'linux':
|
||||
this._platform = 'linux';
|
||||
this.#platform = 'linux';
|
||||
break;
|
||||
case 'win32':
|
||||
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
this.#platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
||||
return;
|
||||
default:
|
||||
assert(false, 'Unsupported platform: ' + platform);
|
||||
@ -245,8 +243,8 @@ export class BrowserFetcher {
|
||||
}
|
||||
|
||||
assert(
|
||||
downloadURLs[this._product][this._platform],
|
||||
'Unsupported platform: ' + this._platform
|
||||
downloadURLs[this.#product][this.#platform],
|
||||
'Unsupported platform: ' + this.#platform
|
||||
);
|
||||
}
|
||||
|
||||
@ -255,7 +253,7 @@ export class BrowserFetcher {
|
||||
* `win32` or `win64`.
|
||||
*/
|
||||
platform(): Platform {
|
||||
return this._platform;
|
||||
return this.#platform;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -263,14 +261,14 @@ export class BrowserFetcher {
|
||||
* `firefox`.
|
||||
*/
|
||||
product(): Product {
|
||||
return this._product;
|
||||
return this.#product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The download host being used.
|
||||
*/
|
||||
host(): string {
|
||||
return this._downloadHost;
|
||||
return this.#downloadHost;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -282,10 +280,10 @@ export class BrowserFetcher {
|
||||
* from the host.
|
||||
*/
|
||||
canDownload(revision: string): Promise<boolean> {
|
||||
const url = downloadURL(
|
||||
this._product,
|
||||
this._platform,
|
||||
this._downloadHost,
|
||||
const url = _downloadURL(
|
||||
this.#product,
|
||||
this.#platform,
|
||||
this.#downloadHost,
|
||||
revision
|
||||
);
|
||||
return new Promise((resolve) => {
|
||||
@ -318,19 +316,19 @@ export class BrowserFetcher {
|
||||
revision: string,
|
||||
progressCallback: (x: number, y: number) => void = (): void => {}
|
||||
): Promise<BrowserFetcherRevisionInfo | undefined> {
|
||||
const url = downloadURL(
|
||||
this._product,
|
||||
this._platform,
|
||||
this._downloadHost,
|
||||
const url = _downloadURL(
|
||||
this.#product,
|
||||
this.#platform,
|
||||
this.#downloadHost,
|
||||
revision
|
||||
);
|
||||
const fileName = url.split('/').pop();
|
||||
assert(fileName, `A malformed download URL was found: ${url}.`);
|
||||
const archivePath = path.join(this._downloadsFolder, fileName);
|
||||
const outputPath = this._getFolderPath(revision);
|
||||
const archivePath = path.join(this.#downloadsFolder, fileName);
|
||||
const outputPath = this.#getFolderPath(revision);
|
||||
if (await existsAsync(outputPath)) return this.revisionInfo(revision);
|
||||
if (!(await existsAsync(this._downloadsFolder)))
|
||||
await mkdirAsync(this._downloadsFolder);
|
||||
if (!(await existsAsync(this.#downloadsFolder)))
|
||||
await mkdirAsync(this.#downloadsFolder);
|
||||
|
||||
// Use system Chromium builds on Linux ARM devices
|
||||
if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
|
||||
@ -338,7 +336,7 @@ export class BrowserFetcher {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await downloadFile(url, archivePath, progressCallback);
|
||||
await _downloadFile(url, archivePath, progressCallback);
|
||||
await install(archivePath, outputPath);
|
||||
} finally {
|
||||
if (await existsAsync(archivePath)) await unlinkAsync(archivePath);
|
||||
@ -355,15 +353,15 @@ export class BrowserFetcher {
|
||||
* available locally on disk.
|
||||
*/
|
||||
async localRevisions(): Promise<string[]> {
|
||||
if (!(await existsAsync(this._downloadsFolder))) return [];
|
||||
const fileNames = await readdirAsync(this._downloadsFolder);
|
||||
if (!(await existsAsync(this.#downloadsFolder))) return [];
|
||||
const fileNames = await readdirAsync(this.#downloadsFolder);
|
||||
return fileNames
|
||||
.map((fileName) => parseFolderPath(this._product, fileName))
|
||||
.map((fileName) => parseFolderPath(this.#product, fileName))
|
||||
.filter(
|
||||
(
|
||||
entry
|
||||
): entry is { product: string; platform: string; revision: string } =>
|
||||
(entry && entry.platform === this._platform) ?? false
|
||||
(entry && entry.platform === this.#platform) ?? false
|
||||
)
|
||||
.map((entry) => entry.revision);
|
||||
}
|
||||
@ -376,7 +374,7 @@ export class BrowserFetcher {
|
||||
* throws if the revision has not been downloaded.
|
||||
*/
|
||||
async remove(revision: string): Promise<void> {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
const folderPath = this.#getFolderPath(revision);
|
||||
assert(
|
||||
await existsAsync(folderPath),
|
||||
`Failed to remove: revision ${revision} is not downloaded`
|
||||
@ -389,33 +387,33 @@ export class BrowserFetcher {
|
||||
* @returns The revision info for the given revision.
|
||||
*/
|
||||
revisionInfo(revision: string): BrowserFetcherRevisionInfo {
|
||||
const folderPath = this._getFolderPath(revision);
|
||||
const folderPath = this.#getFolderPath(revision);
|
||||
let executablePath = '';
|
||||
if (this._product === 'chrome') {
|
||||
if (this._platform === 'mac' || this._platform === 'mac_arm')
|
||||
if (this.#product === 'chrome') {
|
||||
if (this.#platform === 'mac' || this.#platform === 'mac_arm')
|
||||
executablePath = path.join(
|
||||
folderPath,
|
||||
archiveName(this._product, this._platform, revision),
|
||||
archiveName(this.#product, this.#platform, revision),
|
||||
'Chromium.app',
|
||||
'Contents',
|
||||
'MacOS',
|
||||
'Chromium'
|
||||
);
|
||||
else if (this._platform === 'linux')
|
||||
else if (this.#platform === 'linux')
|
||||
executablePath = path.join(
|
||||
folderPath,
|
||||
archiveName(this._product, this._platform, revision),
|
||||
archiveName(this.#product, this.#platform, revision),
|
||||
'chrome'
|
||||
);
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
else if (this.#platform === 'win32' || this.#platform === 'win64')
|
||||
executablePath = path.join(
|
||||
folderPath,
|
||||
archiveName(this._product, this._platform, revision),
|
||||
archiveName(this.#product, this.#platform, revision),
|
||||
'chrome.exe'
|
||||
);
|
||||
else throw new Error('Unsupported platform: ' + this._platform);
|
||||
} else if (this._product === 'firefox') {
|
||||
if (this._platform === 'mac' || this._platform === 'mac_arm')
|
||||
else throw new Error('Unsupported platform: ' + this.#platform);
|
||||
} else if (this.#product === 'firefox') {
|
||||
if (this.#platform === 'mac' || this.#platform === 'mac_arm')
|
||||
executablePath = path.join(
|
||||
folderPath,
|
||||
'Firefox Nightly.app',
|
||||
@ -423,16 +421,16 @@ export class BrowserFetcher {
|
||||
'MacOS',
|
||||
'firefox'
|
||||
);
|
||||
else if (this._platform === 'linux')
|
||||
else if (this.#platform === 'linux')
|
||||
executablePath = path.join(folderPath, 'firefox', 'firefox');
|
||||
else if (this._platform === 'win32' || this._platform === 'win64')
|
||||
else if (this.#platform === 'win32' || this.#platform === 'win64')
|
||||
executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
|
||||
else throw new Error('Unsupported platform: ' + this._platform);
|
||||
} else throw new Error('Unsupported product: ' + this._product);
|
||||
const url = downloadURL(
|
||||
this._product,
|
||||
this._platform,
|
||||
this._downloadHost,
|
||||
else throw new Error('Unsupported platform: ' + this.#platform);
|
||||
} else throw new Error('Unsupported product: ' + this.#product);
|
||||
const url = _downloadURL(
|
||||
this.#product,
|
||||
this.#platform,
|
||||
this.#downloadHost,
|
||||
revision
|
||||
);
|
||||
const local = fs.existsSync(folderPath);
|
||||
@ -442,7 +440,7 @@ export class BrowserFetcher {
|
||||
folderPath,
|
||||
local,
|
||||
url,
|
||||
product: this._product,
|
||||
product: this.#product,
|
||||
});
|
||||
return {
|
||||
revision,
|
||||
@ -450,15 +448,12 @@ export class BrowserFetcher {
|
||||
folderPath,
|
||||
local,
|
||||
url,
|
||||
product: this._product,
|
||||
product: this.#product,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getFolderPath(revision: string): string {
|
||||
return path.resolve(this._downloadsFolder, `${this._platform}-${revision}`);
|
||||
#getFolderPath(revision: string): string {
|
||||
return path.resolve(this.#downloadsFolder, `${this.#platform}-${revision}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,7 +472,7 @@ function parseFolderPath(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function downloadFile(
|
||||
function _downloadFile(
|
||||
url: string,
|
||||
destinationPath: string,
|
||||
progressCallback?: (x: number, y: number) => void
|
||||
@ -524,10 +519,10 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
|
||||
if (archivePath.endsWith('.zip'))
|
||||
return extractZip(archivePath, { dir: folderPath });
|
||||
else if (archivePath.endsWith('.tar.bz2'))
|
||||
return extractTar(archivePath, folderPath);
|
||||
return _extractTar(archivePath, folderPath);
|
||||
else if (archivePath.endsWith('.dmg'))
|
||||
return mkdirAsync(folderPath).then(() =>
|
||||
installDMG(archivePath, folderPath)
|
||||
_installDMG(archivePath, folderPath)
|
||||
);
|
||||
else throw new Error(`Unsupported archive format: ${archivePath}`);
|
||||
}
|
||||
@ -535,7 +530,7 @@ function install(archivePath: string, folderPath: string): Promise<unknown> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
|
||||
function _extractTar(tarPath: string, folderPath: string): Promise<unknown> {
|
||||
return new Promise((fulfill, reject) => {
|
||||
const tarStream = tar.extract(folderPath);
|
||||
tarStream.on('error', reject);
|
||||
@ -548,7 +543,7 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function installDMG(dmgPath: string, folderPath: string): Promise<void> {
|
||||
function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
|
||||
let mountPath: string | undefined;
|
||||
|
||||
return new Promise<void>((fulfill, reject): void => {
|
||||
|
@ -50,19 +50,18 @@ Please check your open processes and ensure that the browser processes that Pupp
|
||||
If you think this is a bug, please report it on the Puppeteer issue tracker.`;
|
||||
|
||||
export class BrowserRunner {
|
||||
private _product: Product;
|
||||
private _executablePath: string;
|
||||
private _processArguments: string[];
|
||||
private _userDataDir: string;
|
||||
private _isTempUserDataDir?: boolean;
|
||||
#product: Product;
|
||||
#executablePath: string;
|
||||
#processArguments: string[];
|
||||
#userDataDir: string;
|
||||
#isTempUserDataDir?: boolean;
|
||||
#closed = true;
|
||||
#listeners: PuppeteerEventListener[] = [];
|
||||
#processClosing!: Promise<void>;
|
||||
|
||||
proc?: childProcess.ChildProcess;
|
||||
connection?: Connection;
|
||||
|
||||
private _closed = true;
|
||||
private _listeners: PuppeteerEventListener[] = [];
|
||||
private _processClosing!: Promise<void>;
|
||||
|
||||
constructor(
|
||||
product: Product,
|
||||
executablePath: string,
|
||||
@ -70,11 +69,11 @@ export class BrowserRunner {
|
||||
userDataDir: string,
|
||||
isTempUserDataDir?: boolean
|
||||
) {
|
||||
this._product = product;
|
||||
this._executablePath = executablePath;
|
||||
this._processArguments = processArguments;
|
||||
this._userDataDir = userDataDir;
|
||||
this._isTempUserDataDir = isTempUserDataDir;
|
||||
this.#product = product;
|
||||
this.#executablePath = executablePath;
|
||||
this.#processArguments = processArguments;
|
||||
this.#userDataDir = userDataDir;
|
||||
this.#isTempUserDataDir = isTempUserDataDir;
|
||||
}
|
||||
|
||||
start(options: LaunchOptions): void {
|
||||
@ -90,11 +89,11 @@ export class BrowserRunner {
|
||||
}
|
||||
assert(!this.proc, 'This process has previously been started.');
|
||||
debugLauncher(
|
||||
`Calling ${this._executablePath} ${this._processArguments.join(' ')}`
|
||||
`Calling ${this.#executablePath} ${this.#processArguments.join(' ')}`
|
||||
);
|
||||
this.proc = childProcess.spawn(
|
||||
this._executablePath,
|
||||
this._processArguments,
|
||||
this.#executablePath,
|
||||
this.#processArguments,
|
||||
{
|
||||
// On non-windows platforms, `detached: true` makes child process a
|
||||
// leader of a new process group, making it possible to kill child
|
||||
@ -109,32 +108,32 @@ export class BrowserRunner {
|
||||
this.proc.stderr?.pipe(process.stderr);
|
||||
this.proc.stdout?.pipe(process.stdout);
|
||||
}
|
||||
this._closed = false;
|
||||
this._processClosing = new Promise((fulfill, reject) => {
|
||||
this.#closed = false;
|
||||
this.#processClosing = new Promise((fulfill, reject) => {
|
||||
this.proc!.once('exit', async () => {
|
||||
this._closed = true;
|
||||
this.#closed = true;
|
||||
// Cleanup as processes exit.
|
||||
if (this._isTempUserDataDir) {
|
||||
if (this.#isTempUserDataDir) {
|
||||
try {
|
||||
await removeFolderAsync(this._userDataDir);
|
||||
await removeFolderAsync(this.#userDataDir);
|
||||
fulfill();
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
if (this._product === 'firefox') {
|
||||
if (this.#product === 'firefox') {
|
||||
try {
|
||||
// When an existing user profile has been used remove the user
|
||||
// preferences file and restore possibly backuped preferences.
|
||||
await unlinkAsync(path.join(this._userDataDir, 'user.js'));
|
||||
await unlinkAsync(path.join(this.#userDataDir, 'user.js'));
|
||||
|
||||
const prefsBackupPath = path.join(
|
||||
this._userDataDir,
|
||||
this.#userDataDir,
|
||||
'prefs.js.puppeteer'
|
||||
);
|
||||
if (fs.existsSync(prefsBackupPath)) {
|
||||
const prefsPath = path.join(this._userDataDir, 'prefs.js');
|
||||
const prefsPath = path.join(this.#userDataDir, 'prefs.js');
|
||||
await unlinkAsync(prefsPath);
|
||||
await renameAsync(prefsBackupPath, prefsPath);
|
||||
}
|
||||
@ -148,29 +147,29 @@ export class BrowserRunner {
|
||||
}
|
||||
});
|
||||
});
|
||||
this._listeners = [
|
||||
this.#listeners = [
|
||||
helper.addEventListener(process, 'exit', this.kill.bind(this)),
|
||||
];
|
||||
if (handleSIGINT)
|
||||
this._listeners.push(
|
||||
this.#listeners.push(
|
||||
helper.addEventListener(process, 'SIGINT', () => {
|
||||
this.kill();
|
||||
process.exit(130);
|
||||
})
|
||||
);
|
||||
if (handleSIGTERM)
|
||||
this._listeners.push(
|
||||
this.#listeners.push(
|
||||
helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
|
||||
);
|
||||
if (handleSIGHUP)
|
||||
this._listeners.push(
|
||||
this.#listeners.push(
|
||||
helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
if (this._closed) return Promise.resolve();
|
||||
if (this._isTempUserDataDir) {
|
||||
if (this.#closed) return Promise.resolve();
|
||||
if (this.#isTempUserDataDir) {
|
||||
this.kill();
|
||||
} else if (this.connection) {
|
||||
// Attempt to close the browser gracefully
|
||||
@ -181,8 +180,8 @@ export class BrowserRunner {
|
||||
}
|
||||
// Cleanup this listener last, as that makes sure the full callback runs. If we
|
||||
// perform this earlier, then the previous function calls would not happen.
|
||||
helper.removeEventListeners(this._listeners);
|
||||
return this._processClosing;
|
||||
helper.removeEventListeners(this.#listeners);
|
||||
return this.#processClosing;
|
||||
}
|
||||
|
||||
kill(): void {
|
||||
@ -226,14 +225,14 @@ export class BrowserRunner {
|
||||
|
||||
// Attempt to remove temporary profile directory to avoid littering.
|
||||
try {
|
||||
if (this._isTempUserDataDir) {
|
||||
removeFolder.sync(this._userDataDir);
|
||||
if (this.#isTempUserDataDir) {
|
||||
removeFolder.sync(this.#userDataDir);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
// Cleanup this listener last, as that makes sure the full callback runs. If we
|
||||
// perform this earlier, then the previous function calls would not happen.
|
||||
helper.removeEventListeners(this._listeners);
|
||||
helper.removeEventListeners(this.#listeners);
|
||||
}
|
||||
|
||||
async setupConnection(options: {
|
||||
|
@ -52,8 +52,17 @@ export interface ProductLauncher {
|
||||
* @internal
|
||||
*/
|
||||
class ChromeLauncher implements ProductLauncher {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_projectRoot: string | undefined;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_preferredRevision: string;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_isPuppeteerCore: boolean;
|
||||
|
||||
constructor(
|
||||
@ -175,7 +184,7 @@ class ChromeLauncher implements ProductLauncher {
|
||||
slowMo,
|
||||
preferredRevision: this._preferredRevision,
|
||||
});
|
||||
browser = await Browser.create(
|
||||
browser = await Browser._create(
|
||||
connection,
|
||||
[],
|
||||
ignoreHTTPSErrors,
|
||||
@ -273,8 +282,17 @@ class ChromeLauncher implements ProductLauncher {
|
||||
* @internal
|
||||
*/
|
||||
class FirefoxLauncher implements ProductLauncher {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_projectRoot: string | undefined;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_preferredRevision: string;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_isPuppeteerCore: boolean;
|
||||
|
||||
constructor(
|
||||
@ -393,7 +411,7 @@ class FirefoxLauncher implements ProductLauncher {
|
||||
slowMo,
|
||||
preferredRevision: this._preferredRevision,
|
||||
});
|
||||
browser = await Browser.create(
|
||||
browser = await Browser._create(
|
||||
connection,
|
||||
[],
|
||||
ignoreHTTPSErrors,
|
||||
|
@ -50,27 +50,27 @@ export class NodeWebSocketTransport implements ConnectionTransport {
|
||||
});
|
||||
}
|
||||
|
||||
private _ws: NodeWebSocket;
|
||||
#ws: NodeWebSocket;
|
||||
onmessage?: (message: NodeWebSocket.Data) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(ws: NodeWebSocket) {
|
||||
this._ws = ws;
|
||||
this._ws.addEventListener('message', (event) => {
|
||||
this.#ws = ws;
|
||||
this.#ws.addEventListener('message', (event) => {
|
||||
if (this.onmessage) this.onmessage.call(null, event.data);
|
||||
});
|
||||
this._ws.addEventListener('close', () => {
|
||||
this.#ws.addEventListener('close', () => {
|
||||
if (this.onclose) this.onclose.call(null);
|
||||
});
|
||||
// Silently ignore all errors - we don't know what to do with them.
|
||||
this._ws.addEventListener('error', () => {});
|
||||
this.#ws.addEventListener('error', () => {});
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
this._ws.send(message);
|
||||
this.#ws.send(message);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._ws.close();
|
||||
this.#ws.close();
|
||||
}
|
||||
}
|
||||
|
@ -22,11 +22,11 @@ import { ConnectionTransport } from '../common/ConnectionTransport.js';
|
||||
import { assert } from '../common/assert.js';
|
||||
|
||||
export class PipeTransport implements ConnectionTransport {
|
||||
_pipeWrite: NodeJS.WritableStream;
|
||||
_eventListeners: PuppeteerEventListener[];
|
||||
#pipeWrite: NodeJS.WritableStream;
|
||||
#eventListeners: PuppeteerEventListener[];
|
||||
|
||||
_isClosed = false;
|
||||
_pendingMessage = '';
|
||||
#isClosed = false;
|
||||
#pendingMessage = '';
|
||||
|
||||
onclose?: () => void;
|
||||
onmessage?: (value: string) => void;
|
||||
@ -35,10 +35,10 @@ export class PipeTransport implements ConnectionTransport {
|
||||
pipeWrite: NodeJS.WritableStream,
|
||||
pipeRead: NodeJS.ReadableStream
|
||||
) {
|
||||
this._pipeWrite = pipeWrite;
|
||||
this._eventListeners = [
|
||||
this.#pipeWrite = pipeWrite;
|
||||
this.#eventListeners = [
|
||||
helper.addEventListener(pipeRead, 'data', (buffer) =>
|
||||
this._dispatch(buffer)
|
||||
this.#dispatch(buffer)
|
||||
),
|
||||
helper.addEventListener(pipeRead, 'close', () => {
|
||||
if (this.onclose) {
|
||||
@ -51,21 +51,21 @@ export class PipeTransport implements ConnectionTransport {
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
assert(!this._isClosed, '`PipeTransport` is closed.');
|
||||
assert(!this.#isClosed, '`PipeTransport` is closed.');
|
||||
|
||||
this._pipeWrite.write(message);
|
||||
this._pipeWrite.write('\0');
|
||||
this.#pipeWrite.write(message);
|
||||
this.#pipeWrite.write('\0');
|
||||
}
|
||||
|
||||
_dispatch(buffer: Buffer): void {
|
||||
assert(!this._isClosed, '`PipeTransport` is closed.');
|
||||
#dispatch(buffer: Buffer): void {
|
||||
assert(!this.#isClosed, '`PipeTransport` is closed.');
|
||||
|
||||
let end = buffer.indexOf('\0');
|
||||
if (end === -1) {
|
||||
this._pendingMessage += buffer.toString();
|
||||
this.#pendingMessage += buffer.toString();
|
||||
return;
|
||||
}
|
||||
const message = this._pendingMessage + buffer.toString(undefined, 0, end);
|
||||
const message = this.#pendingMessage + buffer.toString(undefined, 0, end);
|
||||
if (this.onmessage) {
|
||||
this.onmessage.call(null, message);
|
||||
}
|
||||
@ -79,11 +79,11 @@ export class PipeTransport implements ConnectionTransport {
|
||||
start = end + 1;
|
||||
end = buffer.indexOf('\0', start);
|
||||
}
|
||||
this._pendingMessage = buffer.toString(undefined, start);
|
||||
this.#pendingMessage = buffer.toString(undefined, start);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._isClosed = true;
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this.#isClosed = true;
|
||||
helper.removeEventListeners(this.#eventListeners);
|
||||
}
|
||||
}
|
||||
|
@ -77,12 +77,10 @@ export interface PuppeteerLaunchOptions
|
||||
* @public
|
||||
*/
|
||||
export class PuppeteerNode extends Puppeteer {
|
||||
private _lazyLauncher?: ProductLauncher;
|
||||
private _projectRoot?: string;
|
||||
private __productName?: Product;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
#lazyLauncher?: ProductLauncher;
|
||||
#projectRoot?: string;
|
||||
#productName?: Product;
|
||||
|
||||
_preferredRevision: string;
|
||||
|
||||
/**
|
||||
@ -98,8 +96,8 @@ export class PuppeteerNode extends Puppeteer {
|
||||
const { projectRoot, preferredRevision, productName, ...commonSettings } =
|
||||
settings;
|
||||
super(commonSettings);
|
||||
this._projectRoot = projectRoot;
|
||||
this.__productName = productName;
|
||||
this.#projectRoot = projectRoot;
|
||||
this.#productName = productName;
|
||||
this._preferredRevision = preferredRevision;
|
||||
|
||||
this.connect = this.connect.bind(this);
|
||||
@ -126,13 +124,15 @@ export class PuppeteerNode extends Puppeteer {
|
||||
* @internal
|
||||
*/
|
||||
get _productName(): Product | undefined {
|
||||
return this.__productName;
|
||||
return this.#productName;
|
||||
}
|
||||
|
||||
// don't need any TSDoc here - because the getter is internal the setter is too.
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
set _productName(name: Product | undefined) {
|
||||
if (this.__productName !== name) this._changedProduct = true;
|
||||
this.__productName = name;
|
||||
if (this.#productName !== name) this._changedProduct = true;
|
||||
this.#productName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,8 +184,8 @@ export class PuppeteerNode extends Puppeteer {
|
||||
*/
|
||||
get _launcher(): ProductLauncher {
|
||||
if (
|
||||
!this._lazyLauncher ||
|
||||
this._lazyLauncher.product !== this._productName ||
|
||||
!this.#lazyLauncher ||
|
||||
this.#lazyLauncher.product !== this._productName ||
|
||||
this._changedProduct
|
||||
) {
|
||||
switch (this._productName) {
|
||||
@ -197,14 +197,14 @@ export class PuppeteerNode extends Puppeteer {
|
||||
this._preferredRevision = PUPPETEER_REVISIONS.chromium;
|
||||
}
|
||||
this._changedProduct = false;
|
||||
this._lazyLauncher = Launcher(
|
||||
this._projectRoot,
|
||||
this.#lazyLauncher = Launcher(
|
||||
this.#projectRoot,
|
||||
this._preferredRevision,
|
||||
this._isPuppeteerCore,
|
||||
this._productName
|
||||
);
|
||||
}
|
||||
return this._lazyLauncher;
|
||||
return this.#lazyLauncher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,11 +234,11 @@ export class PuppeteerNode extends Puppeteer {
|
||||
* @returns A new BrowserFetcher instance.
|
||||
*/
|
||||
createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher {
|
||||
if (!this._projectRoot) {
|
||||
if (!this.#projectRoot) {
|
||||
throw new Error(
|
||||
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
|
||||
);
|
||||
}
|
||||
return new BrowserFetcher(this._projectRoot, options);
|
||||
return new BrowserFetcher(this.#projectRoot, options);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ describe('Fixtures', function () {
|
||||
const { defaultBrowserOptions, puppeteerPath } = getTestState();
|
||||
|
||||
let dumpioData = '';
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn } = await import('child_process');
|
||||
const options = Object.assign({}, defaultBrowserOptions, {
|
||||
pipe: true,
|
||||
dumpio: true,
|
||||
@ -44,7 +44,7 @@ describe('Fixtures', function () {
|
||||
const { defaultBrowserOptions, puppeteerPath } = getTestState();
|
||||
|
||||
let dumpioData = '';
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn } = await import('child_process');
|
||||
const options = Object.assign({}, defaultBrowserOptions, { dumpio: true });
|
||||
const res = spawn('node', [
|
||||
path.join(__dirname, 'fixtures', 'dumpio.js'),
|
||||
@ -58,7 +58,7 @@ describe('Fixtures', function () {
|
||||
it('should close the browser when the node process closes', async () => {
|
||||
const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState();
|
||||
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const { spawn, execSync } = await import('child_process');
|
||||
const options = Object.assign({}, defaultBrowserOptions, {
|
||||
// Disable DUMPIO to cleanly read stdout.
|
||||
dumpio: false,
|
||||
|
@ -292,7 +292,7 @@ describe('Frame specs', function () {
|
||||
describe('Frame.client', function () {
|
||||
it('should return the client instance', async () => {
|
||||
const { page } = getTestState();
|
||||
expect(page.mainFrame().client()).toBeInstanceOf(CDPSession);
|
||||
expect(page.mainFrame()._client()).toBeInstanceOf(CDPSession);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -134,7 +134,7 @@ describeChromeOnly('headful tests', function () {
|
||||
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
isPageTarget: (target) => {
|
||||
_isPageTarget(target) {
|
||||
return (
|
||||
target.type === 'other' && target.url.startsWith('devtools://')
|
||||
);
|
||||
|
@ -52,18 +52,17 @@ describe('JSHandle', function () {
|
||||
const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle);
|
||||
expect(isFive).toBeTruthy();
|
||||
});
|
||||
it('should warn on nested object handles', async () => {
|
||||
it('should warn about recursive objects', async () => {
|
||||
const { page } = getTestState();
|
||||
|
||||
const aHandle = await page.evaluateHandle(() => document.body);
|
||||
const test: { obj?: unknown } = {};
|
||||
test.obj = test;
|
||||
let error = null;
|
||||
await page
|
||||
// @ts-expect-error we are deliberately passing a bad type here (nested object)
|
||||
.evaluateHandle((opts) => opts.elem.querySelector('p'), {
|
||||
elem: aHandle,
|
||||
})
|
||||
.evaluateHandle((opts) => opts.elem, { test })
|
||||
.catch((error_) => (error = error_));
|
||||
expect(error.message).toContain('Are you passing a nested JSHandle?');
|
||||
expect(error.message).toContain('Recursive objects are not allowed.');
|
||||
});
|
||||
it('should accept object handle to unserializable value', async () => {
|
||||
const { page } = getTestState();
|
||||
|
@ -21,12 +21,17 @@ import {
|
||||
describeChromeOnly,
|
||||
itFailsFirefox,
|
||||
} from './mocha-utils'; // eslint-disable-line import/extensions
|
||||
import {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
Page,
|
||||
} from '../lib/cjs/puppeteer/api-docs-entry.js';
|
||||
|
||||
describeChromeOnly('OOPIF', function () {
|
||||
/* We use a special browser for this test as we need the --site-per-process flag */
|
||||
let browser;
|
||||
let context;
|
||||
let page;
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
before(async () => {
|
||||
const { puppeteer, defaultBrowserOptions } = getTestState();
|
||||
@ -433,8 +438,8 @@ describeChromeOnly('OOPIF', function () {
|
||||
});
|
||||
});
|
||||
|
||||
function oopifs(context) {
|
||||
function oopifs(context: BrowserContext) {
|
||||
return context
|
||||
.targets()
|
||||
.filter((target) => target._targetInfo.type === 'iframe');
|
||||
.filter((target) => target._getTargetInfo().type === 'iframe');
|
||||
}
|
||||
|
@ -1971,7 +1971,7 @@ describe('Page', function () {
|
||||
describe('Page.client', function () {
|
||||
it('should return the client instance', async () => {
|
||||
const { page } = getTestState();
|
||||
expect(page.client()).toBeInstanceOf(CDPSession);
|
||||
expect(page._client()).toBeInstanceOf(CDPSession);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user